Speed Control: Implement simple speed control

Implements a simple speed control using a resampler
designed for real time changes. A rubberband speed
control will be implemented at a later date.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
This commit is contained in:
Christopher Snowhill 2024-09-20 22:23:59 -07:00
parent 2e5140a321
commit 6a309a3075
18 changed files with 770 additions and 41 deletions

View file

@ -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;

View file

@ -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) {

View file

@ -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;

View file

@ -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

View file

@ -61,6 +61,8 @@
- (void)setVolume:(double)v;
- (void)setSpeed:(double)s;
- (void)setShouldContinue:(BOOL)s;
- (void)setShouldPlayOutBuffer:(BOOL)s;

View file

@ -164,6 +164,10 @@
[output setVolume:v];
}
- (void)setSpeed:(double)s {
[output setSpeed:s];
}
- (void)setShouldContinue:(BOOL)s {
[super setShouldContinue:s];

View file

@ -35,6 +35,8 @@ using std::atomic_long;
#import <stdio.h>
#endif
#import <soxr.h>
@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

View file

@ -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;
}
}

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22154" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23094" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22154"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23094"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -25,23 +25,23 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<splitView dividerStyle="thin" vertical="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2123">
<rect key="frame" x="0.0" y="362" width="1171" height="38"/>
<rect key="frame" x="0.0" y="334" width="1186" height="66"/>
<subviews>
<scrollView fixedFrame="YES" borderType="none" autohidesScrollers="YES" horizontalLineScroll="24" horizontalPageScroll="0.0" verticalLineScroll="24" verticalPageScroll="0.0" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" id="206" userLabel="Scroll View - Playlist View">
<rect key="frame" x="0.0" y="0.0" width="1171" height="38"/>
<rect key="frame" x="0.0" y="0.0" width="1186" height="66"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<clipView key="contentView" drawsBackground="NO" copiesOnScroll="NO" id="KWC-Ti-8KY">
<rect key="frame" x="0.0" y="0.0" width="1171" height="38"/>
<rect key="frame" x="0.0" y="0.0" width="1186" height="66"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView focusRingType="none" verticalHuggingPriority="750" allowsExpansionToolTips="YES" alternatingRowBackgroundColors="YES" autosaveName="Playlist" rowHeight="18" headerView="1517" viewBased="YES" id="207" customClass="PlaylistView">
<rect key="frame" x="0.0" y="0.0" width="1171" height="21"/>
<rect key="frame" x="0.0" y="0.0" width="1186" height="49"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="3" height="6"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn identifier="index" editable="NO" width="63" minWidth="28" maxWidth="64" id="209">
<tableColumn identifier="index" editable="NO" width="64" minWidth="28" maxWidth="64" id="209">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="right" title="#">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.33333299" alpha="1" colorSpace="calibratedWhite"/>
@ -54,11 +54,11 @@
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView id="5N3-SP-Y8z">
<rect key="frame" x="11" y="3" width="68" height="18"/>
<rect key="frame" x="11" y="3" width="69" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="w5u-JQ-3Hf">
<rect key="frame" x="0.0" y="1" width="68" height="16"/>
<rect key="frame" x="0.0" y="1" width="69" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" alignment="right" title="Table View Cell" id="FMU-QZ-NdQ">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@ -95,7 +95,7 @@
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView id="Vw5-xt-0vG">
<rect key="frame" x="82" y="3" width="20" height="17"/>
<rect key="frame" x="83" y="3" width="20" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ERj-i9-caa">
@ -127,7 +127,7 @@
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn identifier="rating" editable="NO" width="106.5" minWidth="48" maxWidth="128" id="208" userLabel="Rating">
<tableColumn identifier="rating" editable="NO" width="109" minWidth="48" maxWidth="128" id="208" userLabel="Rating">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Rating">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.33333299" alpha="1" colorSpace="calibratedWhite"/>
@ -141,11 +141,11 @@
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView id="ZCP-Dx-UBV">
<rect key="frame" x="105" y="3" width="107" height="18"/>
<rect key="frame" x="106" y="3" width="109" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="exY-Bg-Mjm">
<rect key="frame" x="0.0" y="1" width="107" height="16"/>
<rect key="frame" x="0.0" y="1" width="109" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="sdo-Sm-KPH">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@ -171,7 +171,7 @@
</binding>
</connections>
</tableColumn>
<tableColumn identifier="title" editable="NO" width="168.5" minWidth="96" maxWidth="1024" id="XBr-ec-D81">
<tableColumn identifier="title" editable="NO" width="171" minWidth="96" maxWidth="1024" id="XBr-ec-D81">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Title">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.33333299" alpha="1" colorSpace="calibratedWhite"/>
@ -185,11 +185,11 @@
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView id="ZHl-H1-IIC">
<rect key="frame" x="215" y="3" width="168" height="18"/>
<rect key="frame" x="218" y="3" width="171" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="dQP-wC-mba">
<rect key="frame" x="0.0" y="1" width="168" height="16"/>
<rect key="frame" x="0.0" y="1" width="171" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="VVx-99-roJ">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@ -259,7 +259,7 @@
<binding destination="1689" name="fontSize" keyPath="values.fontSize" id="dJs-UO-m5r"/>
</connections>
</tableColumn>
<tableColumn identifier="artist" editable="NO" width="193" minWidth="96" maxWidth="1024" id="391">
<tableColumn identifier="artist" editable="NO" width="195" minWidth="96" maxWidth="1024" id="391">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Artist">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
@ -273,11 +273,11 @@
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView id="gpC-Oe-Rog">
<rect key="frame" x="386" y="3" width="193" height="18"/>
<rect key="frame" x="392" y="3" width="195" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="1WK-qN-Mgj">
<rect key="frame" x="0.0" y="1" width="193" height="16"/>
<rect key="frame" x="0.0" y="1" width="195" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="71l-3L-S3g">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@ -347,7 +347,7 @@
</binding>
</connections>
</tableColumn>
<tableColumn identifier="album" editable="NO" width="193.5" minWidth="96" maxWidth="1024" id="806">
<tableColumn identifier="album" editable="NO" width="195.5" minWidth="96" maxWidth="1024" id="806">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Album">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
@ -361,11 +361,11 @@
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView id="1ed-gX-bct">
<rect key="frame" x="582" y="3" width="194" height="18"/>
<rect key="frame" x="590" y="3" width="196" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="nEt-s5-vRX">
<rect key="frame" x="0.0" y="1" width="194" height="16"/>
<rect key="frame" x="0.0" y="1" width="196" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="moV-3G-GpB">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@ -391,7 +391,7 @@
</binding>
</connections>
</tableColumn>
<tableColumn identifier="length" editable="NO" width="95" minWidth="43.62012" maxWidth="96" id="807">
<tableColumn identifier="length" editable="NO" width="96" minWidth="43.62012" maxWidth="96" id="807">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="right" title="Length">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
@ -404,11 +404,11 @@
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView id="hhB-nv-e78">
<rect key="frame" x="779" y="3" width="95" height="18"/>
<rect key="frame" x="788.5" y="3" width="96" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="tHy-sM-HDB">
<rect key="frame" x="0.0" y="1" width="95" height="16"/>
<rect key="frame" x="0.0" y="1" width="96" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="Igo-5f-yim">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@ -435,7 +435,7 @@
<binding destination="1689" name="fontSize" keyPath="values.fontSize" id="1919"/>
</connections>
</tableColumn>
<tableColumn identifier="year" editable="NO" width="95" minWidth="42" maxWidth="96" id="848">
<tableColumn identifier="year" editable="NO" width="96" minWidth="42" maxWidth="96" id="848">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="right" title="Year">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
@ -448,11 +448,11 @@
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView id="q93-oh-i5T">
<rect key="frame" x="877" y="3" width="95" height="18"/>
<rect key="frame" x="887.5" y="3" width="96" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="bOi-LI-TDx">
<rect key="frame" x="0.0" y="1" width="95" height="16"/>
<rect key="frame" x="0.0" y="1" width="96" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="C2Q-qG-dwX">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@ -475,7 +475,7 @@
<binding destination="1689" name="fontSize" keyPath="values.fontSize" id="1921"/>
</connections>
</tableColumn>
<tableColumn identifier="genre" editable="NO" width="106.5" minWidth="32" maxWidth="512" id="849">
<tableColumn identifier="genre" editable="NO" width="108.5" minWidth="32" maxWidth="512" id="849">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Genre">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
@ -489,11 +489,11 @@
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView id="rRl-p9-Awr">
<rect key="frame" x="975" y="3" width="106" height="18"/>
<rect key="frame" x="986.5" y="3" width="108" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="yW6-2w-6mN">
<rect key="frame" x="0.0" y="1" width="106" height="16"/>
<rect key="frame" x="0.0" y="1" width="108" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="js2-sT-U4M">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@ -516,7 +516,7 @@
<binding destination="1689" name="fontSize" keyPath="values.fontSize" id="1922"/>
</connections>
</tableColumn>
<tableColumn identifier="track" editable="NO" width="71" minWidth="24" maxWidth="72" id="850">
<tableColumn identifier="track" editable="NO" width="72" minWidth="24" maxWidth="72" id="850">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="right" title="№">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
@ -529,11 +529,11 @@
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView id="hgh-VE-5kl">
<rect key="frame" x="1084" y="3" width="75" height="18"/>
<rect key="frame" x="1098" y="3" width="76" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="yEY-MI-d3o">
<rect key="frame" x="0.0" y="1" width="75" height="16"/>
<rect key="frame" x="0.0" y="1" width="76" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="tus-lr-RhS">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@ -848,7 +848,7 @@
<autoresizingMask key="autoresizingMask"/>
</scroller>
<tableHeaderView key="headerView" wantsLayer="YES" id="1517">
<rect key="frame" x="0.0" y="0.0" width="1171" height="17"/>
<rect key="frame" x="0.0" y="0.0" width="1186" height="17"/>
<autoresizingMask key="autoresizingMask"/>
</tableHeaderView>
</scrollView>
@ -861,7 +861,7 @@
</connections>
</splitView>
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="778">
<rect key="frame" x="455" y="4" width="261" height="14"/>
<rect key="frame" x="463" y="4" width="261" height="14"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" alignment="center" title="Total Duration: 00 hours 00 minutes 00 seconds" bezelStyle="round" id="1473">
<font key="font" metaFont="controlContent" size="11"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@ -896,7 +896,7 @@
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<toolbar key="toolbar" implicitIdentifier="B4998081-90DD-45DD-8243-0F7039C7DEA2" displayMode="iconOnly" sizeMode="regular" id="1523">
<toolbar key="toolbar" implicitIdentifier="B4998081-90DD-45DD-8243-0F7039C7DEA2" showsBaselineSeparator="NO" displayMode="iconOnly" sizeMode="regular" id="1523">
<allowedToolbarItems>
<toolbarItem implicitItemIdentifier="NSToolbarSpaceItem" id="1552"/>
<toolbarItem implicitItemIdentifier="NSToolbarFlexibleSpaceItem" id="1529"/>
@ -998,7 +998,7 @@
</textFieldCell>
</textField>
</toolbarItem>
<toolbarItem implicitItemIdentifier="3B680DEB-106E-4549-A478-FFB8A6738053" label="Volume" paletteLabel="Volume" image="volume3Template" visibilityPriority="10" sizingBehavior="auto" id="1610">
<toolbarItem implicitItemIdentifier="3B680DEB-106E-4549-A478-FFB8A6738053" label="Volume" paletteLabel="Volume" image="volume3Template" bordered="YES" visibilityPriority="10" sizingBehavior="auto" id="1610">
<nil key="toolTip"/>
<button key="view" verticalHuggingPriority="750" id="1608" customClass="VolumeButton">
<rect key="frame" x="9" y="14" width="29" height="23"/>
@ -1012,6 +1012,20 @@
</connections>
</button>
</toolbarItem>
<toolbarItem implicitItemIdentifier="7BC995AD-3F93-40E1-AE76-94574D34BCB0" label="Speed" paletteLabel="Speed" image="deskclock" catalog="system" bordered="YES" visibilityPriority="10" sizingBehavior="auto" id="ufn-od-xJF" userLabel="Speed">
<nil key="toolTip"/>
<button key="view" verticalHuggingPriority="750" id="Ta5-Ik-jh9" userLabel="Speed Button" customClass="SpeedButton">
<rect key="frame" x="7" y="14" width="27" height="23"/>
<autoresizingMask key="autoresizingMask"/>
<buttonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" image="deskclock" catalog="system" imagePosition="only" alignment="center" borderStyle="border" inset="2" id="2Al-Cl-n36">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<outlet property="_popView" destination="6P4-yi-9TK" id="3d2-Sw-J9J"/>
</connections>
</button>
</toolbarItem>
<toolbarItem implicitItemIdentifier="2F487D99-16E9-4BF8-9A98-637FABEB2716" label="Info Inspector" paletteLabel="Info Inspector" image="infoTemplate" navigational="YES" id="1629">
<nil key="toolTip"/>
<size key="minSize" width="28" height="23"/>
@ -1175,6 +1189,7 @@
<toolbarItem reference="ZH9-ZU-skw"/>
<toolbarItem reference="1539"/>
<toolbarItem reference="1610"/>
<toolbarItem reference="ufn-od-xJF"/>
<toolbarItem reference="1552"/>
<toolbarItem reference="1568"/>
<toolbarItem reference="1551"/>
@ -1214,7 +1229,7 @@
<rect key="frame" x="0.0" y="0.0" width="581" height="0.0"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<toolbar key="toolbar" implicitIdentifier="35998ECE-5AD8-429E-8479-657249B22C9C" displayMode="iconOnly" sizeMode="regular" id="2222" userLabel="Mini Toolbar">
<toolbar key="toolbar" implicitIdentifier="35998ECE-5AD8-429E-8479-657249B22C9C" showsBaselineSeparator="NO" displayMode="iconOnly" sizeMode="regular" id="2222" userLabel="Mini Toolbar">
<allowedToolbarItems>
<toolbarItem implicitItemIdentifier="NSToolbarSpaceItem" id="2227"/>
<toolbarItem implicitItemIdentifier="NSToolbarFlexibleSpaceItem" id="2228"/>
@ -2343,6 +2358,7 @@ Gw
<outlet property="playlistController" destination="218" id="706"/>
<outlet property="playlistLoader" destination="1319" id="ghZ-65-60L"/>
<outlet property="playlistView" destination="207" id="717"/>
<outlet property="speedSlider" destination="6P4-yi-9TK" id="Xvq-Vj-A88"/>
<outlet property="volumeSlider" destination="1612" id="1615"/>
</connections>
</customObject>
@ -2522,6 +2538,21 @@ Gw
</subviews>
<point key="canvasLocation" x="615" y="-25"/>
</customView>
<customView id="90w-7t-RYP" userLabel="Speed View">
<rect key="frame" x="0.0" y="0.0" width="32" height="168"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<subviews>
<slider horizontalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6P4-yi-9TK" userLabel="Speed Slider" customClass="SpeedSlider">
<rect key="frame" x="6" y="2" width="20" height="164"/>
<autoresizingMask key="autoresizingMask"/>
<sliderCell key="cell" controlSize="small" continuous="YES" alignment="left" minValue="0.20000000000000001" maxValue="5" doubleValue="1" tickMarkPosition="left" sliderType="linear" id="vTw-tV-W5R"/>
<connections>
<action selector="changeSpeed:" target="705" id="FcA-37-2J5"/>
</connections>
</slider>
</subviews>
<point key="canvasLocation" x="694" y="-25"/>
</customView>
<customObject id="1675" customClass="SpotlightWindowController">
<connections>
<outlet property="playlistLoader" destination="1319" id="1694"/>
@ -2587,6 +2618,7 @@ Gw
</objects>
<resources>
<image name="deadItemsTemplate" width="20" height="20"/>
<image name="deskclock" catalog="system" width="15" height="16"/>
<image name="duplicateItemsTemplate" width="20" height="20"/>
<image name="equalizerTemplate" width="20" height="20"/>
<image name="hdcdLogoTemplate" width="656" height="225"/>

View file

@ -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 = "<group>"; };
8399D4E11805A55000B503B1 /* XmlContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XmlContainer.h; sourceTree = "<group>"; };
839B837E286D7F8D00F529EE /* NumberHertzToStringTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NumberHertzToStringTransformer.swift; path = Transformers/NumberHertzToStringTransformer.swift; sourceTree = "<group>"; };
839D48A62C9E73AA00D03298 /* SpeedButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SpeedButton.h; sourceTree = "<group>"; };
839D48A72C9E73AA00D03298 /* SpeedButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SpeedButton.m; sourceTree = "<group>"; };
839D48A82C9E73AA00D03298 /* SpeedSlider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SpeedSlider.h; sourceTree = "<group>"; };
839D48A92C9E73AA00D03298 /* SpeedSlider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SpeedSlider.m; sourceTree = "<group>"; };
839DA7CB274A2D4C001B18E5 /* NSDictionary+Merge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+Merge.h"; sourceTree = "<group>"; };
839DA7CE274A2D4C001B18E5 /* NSDictionary+Merge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+Merge.m"; sourceTree = "<group>"; };
839E3B53286595D700880EA2 /* GeneralPane.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GeneralPane.h; path = Preferences/Preferences/GeneralPane.h; sourceTree = "<group>"; };
@ -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 */,

16
SpeedButton.h Normal file
View file

@ -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 <Cocoa/Cocoa.h>
@interface SpeedButton : NSButton {
IBOutlet SpeedSlider *_popView;
}
@end

51
SpeedButton.m Normal file
View file

@ -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

21
SpeedSlider.h Normal file
View file

@ -0,0 +1,21 @@
//
// SpeedSlider.h
// Cog
//
// Created by Christopher Snowhill on 9/20/24.
// Copyright 2024 __LoSnoCo__. All rights reserved.
//
#import <Cocoa/Cocoa.h>
@interface SpeedSlider : NSSlider {
NSPopover *popover;
NSText *textView;
}
- (void)showToolTip;
- (void)showToolTipForDuration:(NSTimeInterval)duration;
- (void)showToolTipForView:(NSView *)view closeAfter:(NSTimeInterval)duration;
- (void)hideToolTip;
@end

167
SpeedSlider.m Normal file
View file

@ -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

16
Window/SpeedButton.h Normal file
View file

@ -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 <Cocoa/Cocoa.h>
@interface VolumeButton : NSButton {
IBOutlet VolumeSlider *_popView;
}
@end

51
Window/SpeedButton.m Normal file
View file

@ -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

22
Window/SpeedSlider.h Normal file
View file

@ -0,0 +1,22 @@
//
// VolumeSlider.h
// Cog
//
// Created by Vincent Spader on 2/8/09.
// Copyright 2009 __MyCompanyName__. All rights reserved.
//
#import <Cocoa/Cocoa.h>
@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

181
Window/SpeedSlider.m Normal file
View file

@ -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