diff --git a/Application/PlaybackController.h b/Application/PlaybackController.h index 186dfb0aa..ed1f4fcf5 100644 --- a/Application/PlaybackController.h +++ b/Application/PlaybackController.h @@ -19,6 +19,9 @@ #define DEFAULT_VOLUME_DOWN 5 #define DEFAULT_VOLUME_UP DEFAULT_VOLUME_DOWN +#define DEFAULT_SPEED_DOWN 0.2 +#define DEFAULT_SPEED_UP DEFAULT_SPEED_DOWN + extern NSString *CogPlaybackDidBeginNotificiation; extern NSString *CogPlaybackDidPauseNotificiation; extern NSString *CogPlaybackDidResumeNotificiation; @@ -40,6 +43,7 @@ extern NSDictionary *makeRGInfo(PlaylistEntry *pe); IBOutlet EqualizerWindowController *equalizerWindowController; IBOutlet NSSlider *volumeSlider; + IBOutlet NSSlider *speedSlider; IBOutlet NSArrayController *outputDevices; @@ -69,6 +73,10 @@ extern NSDictionary *makeRGInfo(PlaylistEntry *pe); - (IBAction)volumeDown:(id)sender; - (IBAction)volumeUp:(id)sender; +- (IBAction)changeSpeed:(id)sender; +- (IBAction)speedDown:(id)sender; +- (IBAction)speedUp:(id)sender; + - (IBAction)playPauseResume:(id)sender; - (IBAction)pauseResume:(id)sender; - (IBAction)skipToNextAlbum:(id)sender; diff --git a/Application/PlaybackController.m b/Application/PlaybackController.m index 784c9e2b8..27fda07a9 100644 --- a/Application/PlaybackController.m +++ b/Application/PlaybackController.m @@ -91,6 +91,7 @@ NSString *CogPlaybackDidStopNotificiation = @"CogPlaybackDidStopNotificiation"; - (void)initDefaults { NSDictionary *defaultsDictionary = @{ @"volume": @(75.0), + @"speed": @(1.0), @"GraphicEQenable": @(NO), @"GraphicEQpreset": @(-1), @"GraphicEQtrackgenre": @(NO), @@ -109,6 +110,9 @@ NSString *CogPlaybackDidStopNotificiation = @"CogPlaybackDidStopNotificiation"; [volumeSlider setDoubleValue:logarithmicToLinear(volume, MAX_VOLUME)]; [audioPlayer setVolume:volume]; + double speed = [[NSUserDefaults standardUserDefaults] doubleForKey:@"speed"]; + [audioPlayer setSpeed:speed]; + [self setSeekable:NO]; } @@ -478,6 +482,14 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) { } } +- (IBAction)changeSpeed:(id)sender { + DLog(@"SPEED: %lf", [sender doubleValue]); + + [audioPlayer setSpeed:[sender doubleValue]]; + + [[NSUserDefaults standardUserDefaults] setDouble:[audioPlayer speed] forKey:@"speed"]; +} + - (IBAction)skipToNextAlbum:(id)sender { BOOL found = NO; @@ -579,6 +591,20 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) { [[NSUserDefaults standardUserDefaults] setDouble:[audioPlayer volume] forKey:@"volume"]; } +- (IBAction)speedDown:(id)sender { + double newSpeed = [audioPlayer speedDown:DEFAULT_SPEED_DOWN]; + [speedSlider setDoubleValue:[audioPlayer speed]]; + + [[NSUserDefaults standardUserDefaults] setDouble:[audioPlayer speed] forKey:@"speed"]; +} + +- (IBAction)speedUp:(id)sender { + double newSpeed = [audioPlayer speedUp:DEFAULT_SPEED_UP]; + [speedSlider setDoubleValue:[audioPlayer speed]]; + + [[NSUserDefaults standardUserDefaults] setDouble:[audioPlayer speed] forKey:@"speed"]; +} + - (void)audioPlayer:(AudioPlayer *)player displayEqualizer:(AudioUnit)eq { if(_eq && _eq != eq) { diff --git a/Audio/AudioPlayer.h b/Audio/AudioPlayer.h index 081098076..01acaeee4 100644 --- a/Audio/AudioPlayer.h +++ b/Audio/AudioPlayer.h @@ -26,6 +26,7 @@ OutputNode *output; double volume; + double speed; NSMutableArray *chainQueue; @@ -74,6 +75,11 @@ - (double)volumeUp:(double)amount; - (double)volumeDown:(double)amount; +- (void)setSpeed:(double)s; +- (double)speed; +- (double)speedUp:(double)amount; +- (double)speedDown:(double)amount; + - (double)amountPlayed; - (double)amountPlayedInterval; diff --git a/Audio/AudioPlayer.m b/Audio/AudioPlayer.m index 0e378e860..70a858597 100644 --- a/Audio/AudioPlayer.m +++ b/Audio/AudioPlayer.m @@ -75,6 +75,7 @@ } [output setup]; [output setVolume:volume]; + [output setSpeed:speed]; @synchronized(chainQueue) { for(id anObject in chainQueue) { [anObject setShouldContinue:NO]; @@ -210,6 +211,16 @@ return volume; } +- (void)setSpeed:(double)s { + speed = s; + + [output setSpeed:s]; +} + +- (double)speed { + return speed; +} + // This is called by the delegate DURING a requestNextStream request. - (void)setNextStream:(NSURL *)url { [self setNextStream:url withUserInfo:nil withRGInfo:nil]; @@ -648,6 +659,32 @@ return newVolume; } +- (double)speedUp:(double)amount { + const double MAX_SPEED = 5.0; + + double newSpeed; + if((speed + amount) > MAX_SPEED) + newSpeed = MAX_SPEED; + else + newSpeed = speed + amount; + + [self setSpeed:newSpeed]; + return newSpeed; +} + +- (double)speedDown:(double)amount { + const double MIN_SPEED = 0.2; + + double newSpeed; + if((speed - amount) < MIN_SPEED) + newSpeed = MIN_SPEED; + else + newSpeed = speed - amount; + + [self setSpeed:newSpeed]; + return newSpeed; +} + - (void)waitUntilCallbacksExit { // This sucks! And since the thread that's inside the function can be calling // event dispatches, we have to pump the message queue if we're on the main diff --git a/Audio/Chain/OutputNode.h b/Audio/Chain/OutputNode.h index 23e618dad..fdae901f0 100644 --- a/Audio/Chain/OutputNode.h +++ b/Audio/Chain/OutputNode.h @@ -61,6 +61,8 @@ - (void)setVolume:(double)v; +- (void)setSpeed:(double)s; + - (void)setShouldContinue:(BOOL)s; - (void)setShouldPlayOutBuffer:(BOOL)s; diff --git a/Audio/Chain/OutputNode.m b/Audio/Chain/OutputNode.m index 9864e5569..6a67445e1 100644 --- a/Audio/Chain/OutputNode.m +++ b/Audio/Chain/OutputNode.m @@ -164,6 +164,10 @@ [output setVolume:v]; } +- (void)setSpeed:(double)s { + [output setSpeed:s]; +} + - (void)setShouldContinue:(BOOL)s { [super setShouldContinue:s]; diff --git a/Audio/Output/OutputCoreAudio.h b/Audio/Output/OutputCoreAudio.h index ce059ee34..e221478be 100644 --- a/Audio/Output/OutputCoreAudio.h +++ b/Audio/Output/OutputCoreAudio.h @@ -35,6 +35,8 @@ using std::atomic_long; #import #endif +#import + @class OutputNode; @class FSurroundFilter; @@ -54,6 +56,10 @@ using std::atomic_long; double lastClippedSampleRate; + soxr_t rssimplespeed; + double ssRenderedIn, ssLastRenderedIn; + double ssRenderedOut; + void *rsvis; double lastVisRate; @@ -84,6 +90,9 @@ using std::atomic_long; float volume; float eqPreamp; + double speed; + double lastSpeed; + AVAudioFormat *_deviceFormat; AudioDeviceID outputDeviceID; @@ -174,4 +183,6 @@ using std::atomic_long; - (void)reportMotion:(simd_float4x4)matrix; +- (void)setSpeed:(double)s; + @end diff --git a/Audio/Output/OutputCoreAudio.m b/Audio/Output/OutputCoreAudio.m index c7d96f4c7..56c571108 100644 --- a/Audio/Output/OutputCoreAudio.m +++ b/Audio/Output/OutputCoreAudio.m @@ -23,6 +23,8 @@ #import "FSurroundFilter.h" +#define OCTAVES 5 + extern void scale_by_volume(float *buffer, size_t count, float volume); static NSString *CogPlaybackDidBeginNotificiation = @"CogPlaybackDidBeginNotificiation"; @@ -336,6 +338,9 @@ static OSStatus eqRenderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioA outputLock = [[NSLock alloc] init]; + speed = 1.0; + lastSpeed = 1.0; + #ifdef OUTPUT_LOG NSString *logName = [NSTemporaryDirectory() stringByAppendingPathComponent:@"CogAudioLog.raw"]; _logFile = fopen([logName UTF8String], "wb"); @@ -807,6 +812,19 @@ current_device_listener(AudioObjectID inObjectID, UInt32 inNumberAddresses, cons [outputLock unlock]; } + if(rssimplespeed) { + soxr_delete(rssimplespeed); + } + + soxr_error_t error; + soxr_quality_spec_t q_spec = soxr_quality_spec(SOXR_HQ, SOXR_VR); + rssimplespeed = soxr_create(1 << OCTAVES, 1, channels, &error, NULL, &q_spec, NULL); + soxr_set_io_ratio(rssimplespeed, speed, 0); + + ssRenderedIn = 0.0; + ssLastRenderedIn = 0.0; + ssRenderedOut = 0.0; + streamFormat = realStreamFormat; streamFormat.mChannelsPerFrame = channels; streamFormat.mBytesPerFrame = sizeof(float) * channels; @@ -929,6 +947,42 @@ current_device_listener(AudioObjectID inObjectID, UInt32 inNumberAddresses, cons samplePtr = &inputBuffer[0]; if(samplesRendered || fsurround) { + { + int simpleSpeedInput = samplesRendered; + int simpleSpeedRendered = 0; + int channels = realStreamFormat.mChannelsPerFrame; + int max_block_len = 8192; + + if (fabs(speed - lastSpeed) > 1e-5) { + lastSpeed = speed; + soxr_set_io_ratio(rssimplespeed, speed, max_block_len); + } + + const double inputRatio = 1.0 / realStreamFormat.mSampleRate; + const double outputRatio = inputRatio * speed; + + while (simpleSpeedInput > 0) { + int block_len = max_block_len - simpleSpeedRendered; + + if (!block_len) + break; + + float *ibuf = samplePtr; + int len = simpleSpeedInput; + float *obuf = &rsInBuffer[simpleSpeedRendered * channels]; + size_t idone = 0; + size_t odone = 0; + int error = soxr_process(rssimplespeed, ibuf, len, &idone, obuf, block_len, &odone); + simpleSpeedInput -= idone; + ibuf += channels * idone; + simpleSpeedRendered += odone; + ssRenderedIn += idone * inputRatio; + ssRenderedOut += odone * outputRatio; + samplePtr = ibuf; + } + samplePtr = &rsInBuffer[0]; + samplesRendered = simpleSpeedRendered; + } [outputLock lock]; if(fsurround) { int countToProcess = samplesRendered; @@ -1214,9 +1268,13 @@ current_device_listener(AudioObjectID inObjectID, UInt32 inNumberAddresses, cons - (void)updateLatency:(double)secondsPlayed { if(secondsPlayed > 0) { - [outputController incrementAmountPlayed:secondsPlayed]; + double rendered = ssRenderedIn - ssLastRenderedIn; + secondsPlayed = rendered; + ssLastRenderedIn = ssRenderedIn; + [outputController incrementAmountPlayed:rendered]; } - double visLatency = visPushed; + double simpleSpeedLatency = ssRenderedIn - ssRenderedOut; + double visLatency = visPushed + simpleSpeedLatency; visPushed -= secondsPlayed; if(visLatency < secondsPlayed || visLatency > 30.0) { visLatency = secondsPlayed; @@ -1230,6 +1288,10 @@ current_device_listener(AudioObjectID inObjectID, UInt32 inNumberAddresses, cons volume = v * 0.01f; } +- (void)setSpeed:(double)s { + speed = s; +} + - (void)setEqualizerEnabled:(BOOL)enabled { if(enabled && !eqEnabled) { if(_eq) { @@ -1344,6 +1406,10 @@ current_device_listener(AudioObjectID inObjectID, UInt32 inNumberAddresses, cons rsstate_delete(rsvis); rsvis = NULL; } + if(rssimplespeed) { + soxr_delete(rssimplespeed); + rssimplespeed = NULL; + } stopCompleted = YES; } } diff --git a/Base.lproj/MainMenu.xib b/Base.lproj/MainMenu.xib index 26245388e..d9b40e988 100644 --- a/Base.lproj/MainMenu.xib +++ b/Base.lproj/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -25,23 +25,23 @@ - + - + - + - + - + @@ -54,11 +54,11 @@ - + - + @@ -95,7 +95,7 @@ - + @@ -127,7 +127,7 @@ - + @@ -141,11 +141,11 @@ - + - + @@ -171,7 +171,7 @@ - + @@ -185,11 +185,11 @@ - + - + @@ -259,7 +259,7 @@ - + @@ -273,11 +273,11 @@ - + - + @@ -347,7 +347,7 @@ - + @@ -361,11 +361,11 @@ - + - + @@ -391,7 +391,7 @@ - + @@ -404,11 +404,11 @@ - + - + @@ -435,7 +435,7 @@ - + @@ -448,11 +448,11 @@ - + - + @@ -475,7 +475,7 @@ - + @@ -489,11 +489,11 @@ - + - + @@ -516,7 +516,7 @@ - + @@ -529,11 +529,11 @@ - + - + @@ -848,7 +848,7 @@ - + @@ -861,7 +861,7 @@ - + @@ -896,7 +896,7 @@ - + @@ -998,7 +998,7 @@ - + + + + + @@ -1175,6 +1189,7 @@ + @@ -1214,7 +1229,7 @@ - + @@ -2343,6 +2358,7 @@ Gw + @@ -2522,6 +2538,21 @@ Gw + + + + + + + + + + + + + + + @@ -2587,6 +2618,7 @@ Gw + diff --git a/Cog.xcodeproj/project.pbxproj b/Cog.xcodeproj/project.pbxproj index f25d604f4..b02280252 100644 --- a/Cog.xcodeproj/project.pbxproj +++ b/Cog.xcodeproj/project.pbxproj @@ -154,6 +154,8 @@ 83988F0E27BE0A5900A0E89A /* RedundantPlaylistDataStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 83988F0D27BE0A5900A0E89A /* RedundantPlaylistDataStore.m */; }; 8399D4E21805A55000B503B1 /* XmlContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 8399D4E01805A55000B503B1 /* XmlContainer.m */; }; 839B837F286D7F8D00F529EE /* NumberHertzToStringTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 839B837E286D7F8D00F529EE /* NumberHertzToStringTransformer.swift */; }; + 839D48AA2C9E73AA00D03298 /* SpeedButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D48A72C9E73AA00D03298 /* SpeedButton.m */; }; + 839D48AB2C9E73AA00D03298 /* SpeedSlider.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D48A92C9E73AA00D03298 /* SpeedSlider.m */; }; 839DA7CF274A2D4C001B18E5 /* NSDictionary+Merge.m in Sources */ = {isa = PBXBuildFile; fileRef = 839DA7CE274A2D4C001B18E5 /* NSDictionary+Merge.m */; }; 839E56F52879625100DFB5F4 /* SADIE_D02-96000.mhr in Resources */ = {isa = PBXBuildFile; fileRef = 839E56F12879625100DFB5F4 /* SADIE_D02-96000.mhr */; }; 83A360B220E4E81D00192DAB /* Flac.bundle in CopyFiles */ = {isa = PBXBuildFile; fileRef = 8303A30C20E4E3D000951EF8 /* Flac.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -1013,6 +1015,10 @@ 8399D4E01805A55000B503B1 /* XmlContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XmlContainer.m; sourceTree = ""; }; 8399D4E11805A55000B503B1 /* XmlContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XmlContainer.h; sourceTree = ""; }; 839B837E286D7F8D00F529EE /* NumberHertzToStringTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NumberHertzToStringTransformer.swift; path = Transformers/NumberHertzToStringTransformer.swift; sourceTree = ""; }; + 839D48A62C9E73AA00D03298 /* SpeedButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SpeedButton.h; sourceTree = ""; }; + 839D48A72C9E73AA00D03298 /* SpeedButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SpeedButton.m; sourceTree = ""; }; + 839D48A82C9E73AA00D03298 /* SpeedSlider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SpeedSlider.h; sourceTree = ""; }; + 839D48A92C9E73AA00D03298 /* SpeedSlider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SpeedSlider.m; sourceTree = ""; }; 839DA7CB274A2D4C001B18E5 /* NSDictionary+Merge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+Merge.h"; sourceTree = ""; }; 839DA7CE274A2D4C001B18E5 /* NSDictionary+Merge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+Merge.m"; sourceTree = ""; }; 839E3B53286595D700880EA2 /* GeneralPane.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GeneralPane.h; path = Preferences/Preferences/GeneralPane.h; sourceTree = ""; }; @@ -1413,6 +1419,10 @@ 172A12320F5911D20078EF0C /* RepeatTransformers.m */, 172A123A0F5912AE0078EF0C /* ShuffleTransformers.h */, 172A123B0F5912AE0078EF0C /* ShuffleTransformers.m */, + 839D48A62C9E73AA00D03298 /* SpeedButton.h */, + 839D48A72C9E73AA00D03298 /* SpeedButton.m */, + 839D48A82C9E73AA00D03298 /* SpeedSlider.h */, + 839D48A92C9E73AA00D03298 /* SpeedSlider.m */, 17E0D5E70F520F02005B6FED /* TimeField.h */, 17E0D5E80F520F02005B6FED /* TimeField.m */, 17E0D6180F520F9F005B6FED /* VolumeButton.h */, @@ -2551,6 +2561,8 @@ 83A3B734283AE89000CC6593 /* ColorToValueTransformer.m in Sources */, 8D11072D0486CEB800E47090 /* main.m in Sources */, 8E75757109F31D5A0080F1EE /* DNDArrayController.m in Sources */, + 839D48AA2C9E73AA00D03298 /* SpeedButton.m in Sources */, + 839D48AB2C9E73AA00D03298 /* SpeedSlider.m in Sources */, 8E75757209F31D5A0080F1EE /* PlaylistController.m in Sources */, 8E75757309F31D5A0080F1EE /* PlaylistEntry.m in Sources */, 8E75757409F31D5A0080F1EE /* PlaylistView.m in Sources */, diff --git a/SpeedButton.h b/SpeedButton.h new file mode 100644 index 000000000..1f204c704 --- /dev/null +++ b/SpeedButton.h @@ -0,0 +1,16 @@ +// +// SpeedButton.h +// Cog +// +// Created by Christopher Snowhill on 9/20/24. +// Copyright 2024 __LoSnoCo__. All rights reserved. +// + +#import "SpeedSlider.h" +#import + +@interface SpeedButton : NSButton { + IBOutlet SpeedSlider *_popView; +} + +@end diff --git a/SpeedButton.m b/SpeedButton.m new file mode 100644 index 000000000..2b9882856 --- /dev/null +++ b/SpeedButton.m @@ -0,0 +1,51 @@ +// +// SpeedButton.m +// Cog +// +// Created by Christopher Snowhill on 9/20/24. +// Copyright 2024 __LoSnoCo__. All rights reserved. +// + +#import "SpeedButton.h" +#import "PlaybackController.h" + +@implementation SpeedButton { + NSPopover *popover; + NSViewController *viewController; +} + +- (void)awakeFromNib { + popover = [[NSPopover alloc] init]; + popover.behavior = NSPopoverBehaviorTransient; + [popover setContentSize:_popView.bounds.size]; +} + +- (void)scrollWheel:(NSEvent *)theEvent { + if([popover isShown]) { + [_popView scrollWheel:theEvent]; + return; + } + + double change = [theEvent deltaY]; + + [_popView setDoubleValue:[_popView doubleValue] + change]; + + [[_popView target] changeSpeed:_popView]; + + [_popView showToolTipForView:self closeAfter:1.0]; +} + +- (void)mouseDown:(NSEvent *)theEvent { + [popover close]; + + popover.contentViewController = nil; + viewController = [[NSViewController alloc] init]; + viewController.view = _popView; + popover.contentViewController = viewController; + + [popover showRelativeToRect:self.bounds ofView:self preferredEdge:NSRectEdgeMaxY]; + + [super mouseDown:theEvent]; +} + +@end diff --git a/SpeedSlider.h b/SpeedSlider.h new file mode 100644 index 000000000..3ea26df59 --- /dev/null +++ b/SpeedSlider.h @@ -0,0 +1,21 @@ +// +// SpeedSlider.h +// Cog +// +// Created by Christopher Snowhill on 9/20/24. +// Copyright 2024 __LoSnoCo__. All rights reserved. +// + +#import + +@interface SpeedSlider : NSSlider { + NSPopover *popover; + NSText *textView; +} + +- (void)showToolTip; +- (void)showToolTipForDuration:(NSTimeInterval)duration; +- (void)showToolTipForView:(NSView *)view closeAfter:(NSTimeInterval)duration; +- (void)hideToolTip; + +@end diff --git a/SpeedSlider.m b/SpeedSlider.m new file mode 100644 index 000000000..4dd7dc19c --- /dev/null +++ b/SpeedSlider.m @@ -0,0 +1,167 @@ +// +// SpeedSlider.m +// Cog +// +// Created by Christopher Snowhill on 9/20/24. +// Copyright 2024 __LoSnoCo__. All rights reserved. +// + +#import "SpeedSlider.h" +#import "CogAudio/Helper.h" +#import "PlaybackController.h" + +static void *kSpeedSliderContext = &kSpeedSliderContext; + +@implementation SpeedSlider { + NSTimer *currentTimer; + BOOL wasInsideSnapRange; + /*BOOL observersadded;*/ +} + +- (id)initWithFrame:(NSRect)frame { + self = [super initWithFrame:frame]; + return self; +} + +- (id)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + return self; +} + +- (void)awakeFromNib { + wasInsideSnapRange = NO; + textView = [[NSText alloc] init]; + [textView setFrame:NSMakeRect(0, 0, 50, 20)]; + textView.drawsBackground = NO; + textView.editable = NO; + textView.alignment = NSTextAlignmentCenter; + + NSViewController *viewController = [[NSViewController alloc] init]; + viewController.view = textView; + + popover = [[NSPopover alloc] init]; + popover.contentViewController = viewController; + // Don't hide the popover automatically. + popover.behavior = NSPopoverBehaviorTransient; + popover.animates = NO; + [popover setContentSize:textView.bounds.size]; + + /*observersadded = YES;*/ +} + +/*- (void)dealloc { + if(observersadded) { + } +}*/ + +- (void)updateToolTip { + const double value = [self doubleValue]; + NSString *text; + + double speed; + if(value < 0.2) { + speed = 0.2; + } else if(value > 5.0) { + speed = 5.0; + } else { + speed = value; + } + + if(speed < 1) + text = [NSString stringWithFormat:@"%0.2lf×", speed]; + else + text = [NSString stringWithFormat:@"%0.1lf×", speed]; + + [textView setString:text]; +} + +- (void)showToolTip { + [self updateToolTip]; + + double progress = (self.maxValue - [self doubleValue]) / (self.maxValue - self.minValue); + CGFloat width = self.knobThickness - 1; + // Show tooltip to the left of the Slider Knob + CGFloat height = self.knobThickness / 2.f + (self.bounds.size.height - self.knobThickness) * progress - 1; + + NSWindow *window = self.window; + NSPoint screenPoint = [window convertPointToScreen:NSMakePoint(width + 1, height + 1)]; + + if(window.screen.frame.size.width < screenPoint.x + textView.bounds.size.width + 64) // wing it + [popover showRelativeToRect:NSMakeRect(1, height, 2, 2) ofView:self preferredEdge:NSRectEdgeMinX]; + else + [popover showRelativeToRect:NSMakeRect(width, height, 2, 2) ofView:self preferredEdge:NSRectEdgeMaxX]; + [self.window.parentWindow makeKeyWindow]; +} + +- (void)showToolTipForDuration:(NSTimeInterval)duration { + [self showToolTip]; + + [self hideToolTipAfterDelay:duration]; +} + +- (void)showToolTipForView:(NSView *)view closeAfter:(NSTimeInterval)duration { + [self updateToolTip]; + + [popover showRelativeToRect:view.bounds ofView:view preferredEdge:NSRectEdgeMaxY]; + + [self hideToolTipAfterDelay:duration]; +} + +- (void)hideToolTip { + [popover close]; +} + +- (void)hideToolTipAfterDelay:(NSTimeInterval)duration { + if(currentTimer) { + [currentTimer invalidate]; + currentTimer = nil; + } + + if(duration > 0.0) { + currentTimer = [NSTimer scheduledTimerWithTimeInterval:duration + target:self + selector:@selector(hideToolTip) + userInfo:nil + repeats:NO]; + [[NSRunLoop mainRunLoop] addTimer:currentTimer forMode:NSRunLoopCommonModes]; + } +} + +/*- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if(context != kSpeedSliderContext) { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + return; + } +}*/ + +- (BOOL)sendAction:(SEL)theAction to:(id)theTarget { + // Snap to 1.0× if value is close + double snapTarget = 1.0; + double snapProgress = ([self doubleValue] - snapTarget) / (self.maxValue - self.minValue); + + if(fabs(snapProgress) < 0.005) { + [self setDoubleValue:snapTarget]; + if(!wasInsideSnapRange) { + [[NSHapticFeedbackManager defaultPerformer] performFeedbackPattern:NSHapticFeedbackPatternGeneric performanceTime:NSHapticFeedbackPerformanceTimeDefault]; + } + wasInsideSnapRange = YES; + } else { + wasInsideSnapRange = NO; + } + + [self showToolTip]; + + return [super sendAction:theAction to:theTarget]; +} + +- (void)scrollWheel:(NSEvent *)theEvent { + double change = [theEvent deltaY]; + + [self setDoubleValue:[self doubleValue] + change]; + + [[self target] changeSpeed:self]; + + [self showToolTipForDuration:1.0]; +} + +@end diff --git a/Window/SpeedButton.h b/Window/SpeedButton.h new file mode 100644 index 000000000..aa607cd54 --- /dev/null +++ b/Window/SpeedButton.h @@ -0,0 +1,16 @@ +// +// VolumeButton.h +// Cog +// +// Created by Vincent Spader on 2/8/09. +// Copyright 2009 __MyCompanyName__. All rights reserved. +// + +#import "VolumeSlider.h" +#import + +@interface VolumeButton : NSButton { + IBOutlet VolumeSlider *_popView; +} + +@end diff --git a/Window/SpeedButton.m b/Window/SpeedButton.m new file mode 100644 index 000000000..8224d4386 --- /dev/null +++ b/Window/SpeedButton.m @@ -0,0 +1,51 @@ +// +// VolumeButton.m +// Cog +// +// Created by Vincent Spader on 2/8/09. +// Copyright 2009 __MyCompanyName__. All rights reserved. +// + +#import "VolumeButton.h" +#import "PlaybackController.h" + +@implementation VolumeButton { + NSPopover *popover; + NSViewController *viewController; +} + +- (void)awakeFromNib { + popover = [[NSPopover alloc] init]; + popover.behavior = NSPopoverBehaviorTransient; + [popover setContentSize:_popView.bounds.size]; +} + +- (void)scrollWheel:(NSEvent *)theEvent { + if([popover isShown]) { + [_popView scrollWheel:theEvent]; + return; + } + + double change = [theEvent deltaY]; + + [_popView setDoubleValue:[_popView doubleValue] + change]; + + [[_popView target] changeVolume:_popView]; + + [_popView showToolTipForView:self closeAfter:1.0]; +} + +- (void)mouseDown:(NSEvent *)theEvent { + [popover close]; + + popover.contentViewController = nil; + viewController = [[NSViewController alloc] init]; + viewController.view = _popView; + popover.contentViewController = viewController; + + [popover showRelativeToRect:self.bounds ofView:self preferredEdge:NSRectEdgeMaxY]; + + [super mouseDown:theEvent]; +} + +@end diff --git a/Window/SpeedSlider.h b/Window/SpeedSlider.h new file mode 100644 index 000000000..56b959ba0 --- /dev/null +++ b/Window/SpeedSlider.h @@ -0,0 +1,22 @@ +// +// VolumeSlider.h +// Cog +// +// Created by Vincent Spader on 2/8/09. +// Copyright 2009 __MyCompanyName__. All rights reserved. +// + +#import + +@interface VolumeSlider : NSSlider { + NSPopover *popover; + NSText *textView; + double MAX_VOLUME; +} + +- (void)showToolTip; +- (void)showToolTipForDuration:(NSTimeInterval)duration; +- (void)showToolTipForView:(NSView *)view closeAfter:(NSTimeInterval)duration; +- (void)hideToolTip; + +@end diff --git a/Window/SpeedSlider.m b/Window/SpeedSlider.m new file mode 100644 index 000000000..ccaedf7f3 --- /dev/null +++ b/Window/SpeedSlider.m @@ -0,0 +1,181 @@ +// +// VolumeSlider.m +// Cog +// +// Created by Vincent Spader on 2/8/09. +// Copyright 2009 __MyCompanyName__. All rights reserved. +// + +#import "VolumeSlider.h" +#import "CogAudio/Helper.h" +#import "PlaybackController.h" + +static void *kVolumeSliderContext = &kVolumeSliderContext; + +@implementation VolumeSlider { + NSTimer *currentTimer; + BOOL wasInsideSnapRange; + BOOL observersadded; +} + +- (id)initWithFrame:(NSRect)frame { + self = [super initWithFrame:frame]; + return self; +} + +- (id)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + return self; +} + +- (void)awakeFromNib { + BOOL volumeLimit = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] boolForKey:@"volumeLimit"]; + MAX_VOLUME = (volumeLimit) ? 100.0 : 800.0; + + wasInsideSnapRange = NO; + textView = [[NSText alloc] init]; + [textView setFrame:NSMakeRect(0, 0, 50, 20)]; + textView.drawsBackground = NO; + textView.editable = NO; + textView.alignment = NSTextAlignmentCenter; + + NSViewController *viewController = [[NSViewController alloc] init]; + viewController.view = textView; + + popover = [[NSPopover alloc] init]; + popover.contentViewController = viewController; + // Don't hide the popover automatically. + popover.behavior = NSPopoverBehaviorTransient; + popover.animates = NO; + [popover setContentSize:textView.bounds.size]; + + [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.volumeLimit" options:0 context:kVolumeSliderContext]; + observersadded = YES; +} + +- (void)dealloc { + if(observersadded) { + [[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.volumeLimit" context:kVolumeSliderContext]; + } +} + +- (void)updateToolTip { + const double value = [self doubleValue]; + // Sets volume to be the slider value if limit is set to 100% or the actual volume otherwise. + const double volume = (MAX_VOLUME == 100) ? value : linearToLogarithmic(value, MAX_VOLUME); + NSString *text; + + // If volume becomes less than 1%, display two decimal digits of precision (e.g. 0.34%). + if(volume < 1) + text = [NSString stringWithFormat:@"%0.2lf%%", volume]; + // Else if volume becomes less than 10%, display one decimal digit of precision (e.g. 3.4%). + else if(volume < 10) + text = [NSString stringWithFormat:@"%0.1lf%%", volume]; + // Else display no decimal digits. + else + text = [NSString stringWithFormat:@"%0.lf%%", volume]; + + [textView setString:text]; +} + +- (void)showToolTip { + [self updateToolTip]; + + double progress = (self.maxValue - [self doubleValue]) / (self.maxValue - self.minValue); + CGFloat width = self.knobThickness - 1; + // Show tooltip to the left of the Slider Knob + CGFloat height = self.knobThickness / 2.f + (self.bounds.size.height - self.knobThickness) * progress - 1; + + NSWindow *window = self.window; + NSPoint screenPoint = [window convertPointToScreen:NSMakePoint(width + 1, height + 1)]; + + if(window.screen.frame.size.width < screenPoint.x + textView.bounds.size.width + 64) // wing it + [popover showRelativeToRect:NSMakeRect(1, height, 2, 2) ofView:self preferredEdge:NSRectEdgeMinX]; + else + [popover showRelativeToRect:NSMakeRect(width, height, 2, 2) ofView:self preferredEdge:NSRectEdgeMaxX]; + [self.window.parentWindow makeKeyWindow]; +} + +- (void)showToolTipForDuration:(NSTimeInterval)duration { + [self showToolTip]; + + [self hideToolTipAfterDelay:duration]; +} + +- (void)showToolTipForView:(NSView *)view closeAfter:(NSTimeInterval)duration { + [self updateToolTip]; + + [popover showRelativeToRect:view.bounds ofView:view preferredEdge:NSRectEdgeMaxY]; + + [self hideToolTipAfterDelay:duration]; +} + +- (void)hideToolTip { + [popover close]; +} + +- (void)hideToolTipAfterDelay:(NSTimeInterval)duration { + if(currentTimer) { + [currentTimer invalidate]; + currentTimer = nil; + } + + if(duration > 0.0) { + currentTimer = [NSTimer scheduledTimerWithTimeInterval:duration + target:self + selector:@selector(hideToolTip) + userInfo:nil + repeats:NO]; + [[NSRunLoop mainRunLoop] addTimer:currentTimer forMode:NSRunLoopCommonModes]; + } +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if(context != kVolumeSliderContext) { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + return; + } + + if([keyPath isEqualToString:@"values.volumeLimit"]) { + BOOL volumeLimit = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] boolForKey:@"volumeLimit"]; + const double new_MAX_VOLUME = (volumeLimit) ? 100.0 : 800.0; + + if(MAX_VOLUME != new_MAX_VOLUME) { + double currentLevel = linearToLogarithmic([self doubleValue], MAX_VOLUME); + [self setDoubleValue:logarithmicToLinear(currentLevel, new_MAX_VOLUME)]; + } + MAX_VOLUME = new_MAX_VOLUME; + } +} + +- (BOOL)sendAction:(SEL)theAction to:(id)theTarget { + // Snap to 100% if value is close + double snapTarget = logarithmicToLinear(100.0, MAX_VOLUME); + double snapProgress = ([self doubleValue] - snapTarget) / (self.maxValue - self.minValue); + + if(fabs(snapProgress) < 0.005) { + [self setDoubleValue:snapTarget]; + if(!wasInsideSnapRange) { + [[NSHapticFeedbackManager defaultPerformer] performFeedbackPattern:NSHapticFeedbackPatternGeneric performanceTime:NSHapticFeedbackPerformanceTimeDefault]; + } + wasInsideSnapRange = YES; + } else { + wasInsideSnapRange = NO; + } + + [self showToolTip]; + + return [super sendAction:theAction to:theTarget]; +} + +- (void)scrollWheel:(NSEvent *)theEvent { + double change = [theEvent deltaY]; + + [self setDoubleValue:[self doubleValue] + change]; + + [[self target] changeVolume:self]; + + [self showToolTipForDuration:1.0]; +} + +@end