From c4d242472aa6ee7b1ce3aee9b452210d7ad5ed39 Mon Sep 17 00:00:00 2001 From: Chris Ballinger Date: Tue, 30 Jan 2018 23:48:23 -0800 Subject: [PATCH 1/7] Working on Sounds player --- HapticKey.xcodeproj/project.pbxproj | 6 ++ HapticKey/Classes/HTKSounds.h | 49 ++++++++++ HapticKey/Classes/HTKSounds.m | 142 ++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 HapticKey/Classes/HTKSounds.h create mode 100644 HapticKey/Classes/HTKSounds.m diff --git a/HapticKey.xcodeproj/project.pbxproj b/HapticKey.xcodeproj/project.pbxproj index 70bbd29..910c7d9 100644 --- a/HapticKey.xcodeproj/project.pbxproj +++ b/HapticKey.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 54FBCF291FD4CDE0000EB4D3 /* HTKMultitouchActuator.m in Sources */ = {isa = PBXBuildFile; fileRef = 54FBCF281FD4CDE0000EB4D3 /* HTKMultitouchActuator.m */; }; 54FBCF321FD52483000EB4D3 /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 54FBCF301FD52483000EB4D3 /* AppIcon.icns */; }; 54FBCF331FD52483000EB4D3 /* StatusItem.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 54FBCF311FD52483000EB4D3 /* StatusItem.pdf */; }; + D978A86E20218E8100F9810F /* HTKSounds.m in Sources */ = {isa = PBXBuildFile; fileRef = D978A86D20218E8100F9810F /* HTKSounds.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -62,6 +63,8 @@ 54FBCF281FD4CDE0000EB4D3 /* HTKMultitouchActuator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HTKMultitouchActuator.m; sourceTree = ""; }; 54FBCF301FD52483000EB4D3 /* AppIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = AppIcon.icns; sourceTree = ""; }; 54FBCF311FD52483000EB4D3 /* StatusItem.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = StatusItem.pdf; sourceTree = ""; }; + D978A86C20218E8100F9810F /* HTKSounds.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HTKSounds.h; sourceTree = ""; }; + D978A86D20218E8100F9810F /* HTKSounds.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HTKSounds.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -117,6 +120,8 @@ 54CDDBF11FE44F33005F35A1 /* Classes */ = { isa = PBXGroup; children = ( + D978A86C20218E8100F9810F /* HTKSounds.h */, + D978A86D20218E8100F9810F /* HTKSounds.m */, 548E15391FD00412001C0D4C /* HTKAppDelegate.h */, 548E153A1FD00412001C0D4C /* HTKAppDelegate.m */, 5443CEFD1FE38F6C002D4086 /* HTKEvent.h */, @@ -233,6 +238,7 @@ 548E15431FD00412001C0D4C /* main.m in Sources */, 544C76362017311500FF155C /* HTKLoginItem.m in Sources */, 5443CF081FE39293002D4086 /* HTKTapGestureEventListener.m in Sources */, + D978A86E20218E8100F9810F /* HTKSounds.m in Sources */, 548E153B1FD00412001C0D4C /* HTKAppDelegate.m in Sources */, 5443CEFC1FE38EA8002D4086 /* HTKEventListener.m in Sources */, 54ACC711201F15F70026CAFD /* HTKSystemSound.m in Sources */, diff --git a/HapticKey/Classes/HTKSounds.h b/HapticKey/Classes/HTKSounds.h new file mode 100644 index 0000000..ff1a1f6 --- /dev/null +++ b/HapticKey/Classes/HTKSounds.h @@ -0,0 +1,49 @@ +// +// HTKSounds.h +// HapticKey +// +// Created by Chris Ballinger on 1/30/18. +// Copyright © 2018 Yoshimasa Niwa. All rights reserved. +// + +@import Foundation; +@import AVFoundation; + +NS_ASSUME_NONNULL_BEGIN + +/** + * This class helps manage the custom sound files available for playback. + */ +@interface HTKSounds : NSObject + +/** Current sound file directory */ +@property (nonatomic, readonly) NSString *path; +/** Full paths to all potential sound files in this directory */ +@property (nonatomic, readonly) NSArray *allSoundFiles; + +@property (nonatomic, readonly, nullable) AVAudioPlayer *fingerUp; +@property (nonatomic, readonly, nullable) AVAudioPlayer *fingerDown; + ++ (instancetype)new NS_UNAVAILABLE; +- (instancetype)init NS_UNAVAILABLE; + +/** Path to directory containing playable sound files. Will be created if directory doesn't exist. */ +- (nullable instancetype)initWithPath:(NSString *)path error:(NSError**)error NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithDefaultPath; + +/** Reloads fingerUp and fingerDown players */ +- (void) reloadPlayers; + +// MARK: - Class Properties + +/** ~/Library/Application Support/HapticKey/Sounds/ */ +@property (nonatomic, class, readonly) NSString *defaultPath; + +/** finger down sound file path stored in user defaults */ +@property (nonatomic, class, nullable) NSString *fingerUpFilePath; +/** finger up sound file path stored in user defaults */ +@property (nonatomic, class, nullable) NSString *fingerDownFilePath; + +@end + +NS_ASSUME_NONNULL_END diff --git a/HapticKey/Classes/HTKSounds.m b/HapticKey/Classes/HTKSounds.m new file mode 100644 index 0000000..8a367ef --- /dev/null +++ b/HapticKey/Classes/HTKSounds.m @@ -0,0 +1,142 @@ +// +// HTKSounds.m +// HapticKey +// +// Created by Chris Ballinger on 1/30/18. +// Copyright © 2018 Yoshimasa Niwa. All rights reserved. +// + +#import "HTKSounds.h" + +@import os.log; + +NS_ASSUME_NONNULL_BEGIN + +static NSString * const kDefaultSystemSoundFilePath = @"/System/Library/Components/CoreAudio.component/Contents/SharedSupport/SystemSounds/ink/InkSoundBecomeMouse.aif"; + +static NSString * const kFingerUpFilePathKey = @"FingerUpFilePath"; +static NSString * const kFingerDownFilePathKey = @"FingerDownFilePath"; + +@implementation HTKSounds + +// MARK: - Init + +- (instancetype)init +{ + [self doesNotRecognizeSelector:_cmd]; + abort(); +} + +- (nullable instancetype)initWithPath:(NSString *)path error:(NSError**)error +{ + NSParameterAssert(path); + if (self = [super init]) { + _path = [path copy]; + if (![self createDirectoryIfNeeded:error]) { + return nil; + } + [self reloadPlayers]; + } + return self; +} + +- (nullable instancetype)initWithDefaultPath { + return [self initWithPath:self.class.defaultPath error:nil]; +} + +- (void) reloadPlayers { + _fingerUp = nil; + _fingerDown = nil; + NSError *error = nil; + NSString *fingerUpFilePath = self.class.fingerUpFilePath; + NSString *fingerDownFilePath = self.class.fingerDownFilePath; + if (fingerUpFilePath) { + NSURL *fileURL = [NSURL fileURLWithPath:fingerUpFilePath]; + _fingerUp = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&error]; + } + if (fingerDownFilePath) { + NSURL *fileURL = [NSURL fileURLWithPath:fingerUpFilePath]; + _fingerDown = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&error]; + } +} + +- (NSArray*) allSoundFiles { + NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:[NSURL fileURLWithPath:self.path] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:nil]; + if (!contents.count) { + // we must populate an initial sound + [self populateDefaultSounds]; + contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:[NSURL fileURLWithPath:self.path] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:nil]; + } + return contents; +} + +// MARK: - Private + +- (BOOL) createDirectoryIfNeeded:(NSError**)error { + // create directory if it isn't already + BOOL isDirectory = NO; + BOOL exists = [NSFileManager.defaultManager fileExistsAtPath:_path isDirectory:&isDirectory]; + if (exists && !isDirectory) { + // path must be a directory + return NO; + } else if (!exists) { + return [NSFileManager.defaultManager createDirectoryAtPath:_path withIntermediateDirectories:YES attributes:nil error:error]; + } else { + return YES; + } +} + +- (BOOL) populateDefaultSounds { + // we must populate an initial sound + // TODO: use proper localized string + NSString *fileName = [NSLocalizedString(@"Default", @"default audio file") stringByAppendingPathExtension:@"aif"]; + if (!fileName) { + return NO; + } + NSString *newPath = [self.path stringByAppendingPathComponent:fileName]; + BOOL result = [NSFileManager.defaultManager copyItemAtPath:kDefaultSystemSoundFilePath toPath:newPath error:nil]; + if (!result) { + os_log_error(OS_LOG_DEFAULT, "Fail to create default system sound at path: %{public}@", newPath); + return NO; + } + return YES; +} + +// MARK: - Class Properties + ++ (NSString*) defaultPath { + NSString *applicationSupportDirectory = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES).lastObject; + NSString *applicationName = NSBundle.mainBundle.infoDictionary[(NSString*)kCFBundleNameKey]; + NSString *appDirectory = [applicationSupportDirectory stringByAppendingPathComponent:applicationName]; + NSString *soundsDirectory = [appDirectory stringByAppendingPathComponent:@"Sounds"]; + NSParameterAssert(soundsDirectory); + return soundsDirectory; +} + ++ (void) setFingerUpFilePath:(nullable NSString *)fingerUpFilePath { + if (fingerUpFilePath) { + [NSUserDefaults.standardUserDefaults setObject:fingerUpFilePath forKey:kFingerUpFilePathKey]; + } else { + [NSUserDefaults.standardUserDefaults removeObjectForKey:kFingerUpFilePathKey]; + } +} + ++ (nullable NSString*) fingerUpFilePath { + return [NSUserDefaults.standardUserDefaults stringForKey:kFingerUpFilePathKey]; +} + ++ (void) setFingerDownFilePath:(nullable NSString *)fingerDownFilePath { + if (fingerDownFilePath) { + [NSUserDefaults.standardUserDefaults setObject:fingerDownFilePath forKey:kFingerDownFilePathKey]; + } else { + [NSUserDefaults.standardUserDefaults removeObjectForKey:kFingerDownFilePathKey]; + } +} + ++ (nullable NSString*) fingerDownFilePath { + return [NSUserDefaults.standardUserDefaults stringForKey:kFingerDownFilePathKey]; +} + +@end + +NS_ASSUME_NONNULL_END From 4b2e46e1fd4ec55f4dfbc84153806dd5fc17c258 Mon Sep 17 00:00:00 2001 From: Chris Ballinger Date: Wed, 31 Jan 2018 13:07:27 -0800 Subject: [PATCH 2/7] Split the default sound file into separate up and down sounds --- HapticKey/Classes/HTKAppDelegate.m | 3 +- HapticKey/Classes/HTKHapticFeedback.h | 2 + HapticKey/Classes/HTKHapticFeedback.m | 22 +--- HapticKey/Classes/HTKSounds.h | 26 ++++- HapticKey/Classes/HTKSounds.m | 162 ++++++++++++++++++++++++-- 5 files changed, 181 insertions(+), 34 deletions(-) diff --git a/HapticKey/Classes/HTKAppDelegate.m b/HapticKey/Classes/HTKAppDelegate.m index a7e50e1..1b1ccc5 100644 --- a/HapticKey/Classes/HTKAppDelegate.m +++ b/HapticKey/Classes/HTKAppDelegate.m @@ -11,6 +11,7 @@ #import "HTKHapticFeedback.h" #import "HTKLoginItem.h" #import "HTKTapGestureEventListener.h" +#import "HTKSounds.h" NS_ASSUME_NONNULL_BEGIN @@ -292,7 +293,7 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification [self _htk_main_loadMainBundleLoginItem]; self.finishedLaunching = YES; - + [self _htk_main_updateUserDefaults]; [self _htk_main_updateStatusItem]; [self _htk_main_updateHapticFeedback]; diff --git a/HapticKey/Classes/HTKHapticFeedback.h b/HapticKey/Classes/HTKHapticFeedback.h index a7d5575..abd87d5 100644 --- a/HapticKey/Classes/HTKHapticFeedback.h +++ b/HapticKey/Classes/HTKHapticFeedback.h @@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN @class HTKEventListener; +@class HTKSounds; typedef NS_ENUM(NSUInteger, HTKHapticFeedbackType) { HTKHapticFeedbackTypeNone, @@ -30,6 +31,7 @@ typedef NS_ENUM(NSUInteger, HTKSoundFeedbackType) { @property (nonatomic, getter=isEnabled) BOOL enabled; @property (nonatomic) HTKHapticFeedbackType type; @property (nonatomic) HTKSoundFeedbackType soundType; +@property (nonatomic, readonly) HTKSounds *sounds; @property (nonatomic, getter=isScreenFlashEnabled) BOOL screenFlashEnabled; + (instancetype)new NS_UNAVAILABLE; diff --git a/HapticKey/Classes/HTKHapticFeedback.m b/HapticKey/Classes/HTKHapticFeedback.m index 8532a7a..277a6ae 100644 --- a/HapticKey/Classes/HTKHapticFeedback.m +++ b/HapticKey/Classes/HTKHapticFeedback.m @@ -12,6 +12,7 @@ #import "HTKMultitouchActuator.h" #import "HTKSystemSound.h" #import "HTKTimer.h" +#import "HTKSounds.h" @import AudioToolbox; @@ -25,7 +26,6 @@ @interface HTKHapticFeedback () @property (nonatomic, nullable) HTKTimer *timer; -@property (nonatomic, readonly) HTKSystemSound *defaultSystemSound; @end @@ -44,7 +44,8 @@ - (instancetype)initWithEventListener:(HTKEventListener *)eventListener _eventListener.delegate = self; _type = HTKHapticFeedbackTypeMedium; - _defaultSystemSound = [[HTKSystemSound alloc] initWithSystemSoundsGroup:kDefaultSystemSoundsGroup name:kDefaultSystemSoundsName]; + _sounds = [[HTKSounds alloc] initWithDefaultPath]; + [_sounds resetDefaultSounds]; } return self; } @@ -70,15 +71,12 @@ - (void)eventListener:(HTKEventListener *)eventListener didListenEvent:(HTKEvent self.timer = [[HTKTimer alloc] initWithInterval:kMinimumActuationInterval target:self selector:@selector(_htk_timer_didFire:)]; const SInt32 actuationID = [self _htk_main_actuationID]; - HTKSystemSound * const systemSound = [self _htk_main_systemSound]; switch (event.phase) { case HTKEventPhaseBegin: if (actuationID != 0) { [[HTKMultitouchActuator sharedActuator] actuateActuationID:actuationID unknown1:0 unknown2:0.0 unknown3:2.0]; } - if (systemSound) { - [systemSound play]; - } + [self.sounds playFingerDown]; if (self.screenFlashEnabled) { AudioServicesPlaySystemSoundWithCompletion(kSystemSoundID_FlashScreen, NULL); } @@ -87,6 +85,7 @@ - (void)eventListener:(HTKEventListener *)eventListener didListenEvent:(HTKEvent if (actuationID != 0) { [[HTKMultitouchActuator sharedActuator] actuateActuationID:actuationID unknown1:0 unknown2:0.0 unknown3:0.0]; } + [self.sounds playFingerUp]; break; } } @@ -117,17 +116,6 @@ - (SInt32)_htk_main_actuationID return 0; } -- (nullable HTKSystemSound *)_htk_main_systemSound -{ - switch (self.soundType) { - case HTKSoundFeedbackTypeNone: - return nil; - case HTKSoundFeedbackTypeDefault: - return self.defaultSystemSound; - } - return nil; -} - @end NS_ASSUME_NONNULL_END diff --git a/HapticKey/Classes/HTKSounds.h b/HapticKey/Classes/HTKSounds.h index ff1a1f6..2a01fd4 100644 --- a/HapticKey/Classes/HTKSounds.h +++ b/HapticKey/Classes/HTKSounds.h @@ -7,7 +7,6 @@ // @import Foundation; -@import AVFoundation; NS_ASSUME_NONNULL_BEGIN @@ -16,13 +15,14 @@ NS_ASSUME_NONNULL_BEGIN */ @interface HTKSounds : NSObject +// MARK: Properties + /** Current sound file directory */ @property (nonatomic, readonly) NSString *path; /** Full paths to all potential sound files in this directory */ @property (nonatomic, readonly) NSArray *allSoundFiles; -@property (nonatomic, readonly, nullable) AVAudioPlayer *fingerUp; -@property (nonatomic, readonly, nullable) AVAudioPlayer *fingerDown; +// MARK: Init + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; @@ -31,14 +31,30 @@ NS_ASSUME_NONNULL_BEGIN - (nullable instancetype)initWithPath:(NSString *)path error:(NSError**)error NS_DESIGNATED_INITIALIZER; - (nullable instancetype)initWithDefaultPath; -/** Reloads fingerUp and fingerDown players */ +// MARK: Public Methods + +/** Reloads fingerUp and fingerDown players from user preferences */ - (void) reloadPlayers; -// MARK: - Class Properties +/** Play sound for finger up, if enabled */ +- (void) playFingerUp; +/** Play sound for finger down, if enabled */ +- (void) playFingerDown; + +/** Resets to use default sounds */ +- (BOOL) resetDefaultSounds; + +// MARK: Class Properties /** ~/Library/Application Support/HapticKey/Sounds/ */ @property (nonatomic, class, readonly) NSString *defaultPath; +@end + +// MARK: - User Defaults + +@interface HTKSounds (UserDefaults) + /** finger down sound file path stored in user defaults */ @property (nonatomic, class, nullable) NSString *fingerUpFilePath; /** finger up sound file path stored in user defaults */ diff --git a/HapticKey/Classes/HTKSounds.m b/HapticKey/Classes/HTKSounds.m index 8a367ef..019ac0b 100644 --- a/HapticKey/Classes/HTKSounds.m +++ b/HapticKey/Classes/HTKSounds.m @@ -8,15 +8,24 @@ #import "HTKSounds.h" +@import AudioToolbox; @import os.log; +@import AVFoundation; NS_ASSUME_NONNULL_BEGIN static NSString * const kDefaultSystemSoundFilePath = @"/System/Library/Components/CoreAudio.component/Contents/SharedSupport/SystemSounds/ink/InkSoundBecomeMouse.aif"; +//static NSString * const kDefaultSystemSoundFilePath = @"/System/Library/Components/CoreAudio.component/Contents/SharedSupport/SystemSounds/system/Media Keys.aif"; + static NSString * const kFingerUpFilePathKey = @"FingerUpFilePath"; static NSString * const kFingerDownFilePathKey = @"FingerDownFilePath"; +@interface HTKSounds() +@property (nonatomic, readonly, nullable) AVAudioPlayer *fingerUp; +@property (nonatomic, readonly, nullable) AVAudioPlayer *fingerDown; +@end + @implementation HTKSounds // MARK: - Init @@ -44,18 +53,29 @@ - (nullable instancetype)initWithDefaultPath { return [self initWithPath:self.class.defaultPath error:nil]; } +// MARK: - Public + +- (void) playFingerUp { + [self.fingerUp play]; +} + +- (void) playFingerDown { + [self.fingerDown play]; +} + - (void) reloadPlayers { _fingerUp = nil; _fingerDown = nil; NSError *error = nil; NSString *fingerUpFilePath = self.class.fingerUpFilePath; NSString *fingerDownFilePath = self.class.fingerDownFilePath; + if (fingerUpFilePath) { NSURL *fileURL = [NSURL fileURLWithPath:fingerUpFilePath]; _fingerUp = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&error]; } if (fingerDownFilePath) { - NSURL *fileURL = [NSURL fileURLWithPath:fingerUpFilePath]; + NSURL *fileURL = [NSURL fileURLWithPath:fingerDownFilePath]; _fingerDown = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&error]; } } @@ -64,7 +84,7 @@ - (void) reloadPlayers { NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:[NSURL fileURLWithPath:self.path] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:nil]; if (!contents.count) { // we must populate an initial sound - [self populateDefaultSounds]; + [self resetDefaultSounds]; contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:[NSURL fileURLWithPath:self.path] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:nil]; } return contents; @@ -86,22 +106,134 @@ - (BOOL) createDirectoryIfNeeded:(NSError**)error { } } -- (BOOL) populateDefaultSounds { - // we must populate an initial sound - // TODO: use proper localized string - NSString *fileName = [NSLocalizedString(@"Default", @"default audio file") stringByAppendingPathExtension:@"aif"]; - if (!fileName) { +- (BOOL) resetDefaultSounds { + + NSURL *originalFile = [NSURL fileURLWithPath:kDefaultSystemSoundFilePath]; + + AudioFileID inAudioFile = NULL; + AudioFileID downFile = NULL; + AudioFileID upFile = NULL; + dispatch_block_t closeAudioFiles = ^{ + if (inAudioFile) { + AudioFileClose(inAudioFile); + } + if (downFile) { + AudioFileClose(downFile); + } + if (upFile) { + AudioFileClose(upFile); + } + }; + + OSStatus result = AudioFileOpenURL((__bridge CFURLRef)originalFile, kAudioFileReadPermission, 0, &inAudioFile); + if (result != noErr) { + os_log_error(OS_LOG_DEFAULT, "Error opening system audio file at path %{public}@ %d %@", originalFile.path, result, UTCreateStringForOSType(result)); + closeAudioFiles(); return NO; + } else { + os_log_info(OS_LOG_DEFAULT, "Opened system audio file at path %{public}@", originalFile.path); } - NSString *newPath = [self.path stringByAppendingPathComponent:fileName]; - BOOL result = [NSFileManager.defaultManager copyItemAtPath:kDefaultSystemSoundFilePath toPath:newPath error:nil]; - if (!result) { - os_log_error(OS_LOG_DEFAULT, "Fail to create default system sound at path: %{public}@", newPath); + + AudioStreamBasicDescription asbd = {0}; + UInt32 specifierSize = sizeof(asbd); + result = AudioFileGetProperty(inAudioFile, kAudioFilePropertyDataFormat, &specifierSize, &asbd); + NSAssert2(noErr == result, @"Error getting absd for system audio file %d %@", result, UTCreateStringForOSType(result)); + if (result != noErr) { + os_log_error(OS_LOG_DEFAULT, "Error getting absd for system audio file %d %@", result, UTCreateStringForOSType(result)); + closeAudioFiles(); return NO; } + + UInt64 totalBytes = 0; + specifierSize = sizeof(totalBytes); + result = AudioFileGetProperty(inAudioFile, kAudioFilePropertyAudioDataByteCount, &specifierSize, &totalBytes); + NSAssert1(noErr == result, @"Error getting audio byte count %d", result); + UInt64 packetCount = 0; + specifierSize = sizeof(packetCount); + result = AudioFileGetProperty(inAudioFile, kAudioFilePropertyAudioDataPacketCount, &specifierSize, &packetCount); + NSAssert1(noErr == result, @"Error getting audio packet count %d", result); + UInt32 packetSize = 0; + specifierSize = sizeof(packetSize); + result = AudioFileGetProperty(inAudioFile, kAudioFilePropertyMaximumPacketSize, &specifierSize, &packetSize); + NSAssert1(noErr == result, @"Error getting audio packet size %d", result); + + + NSURL* (^createOutURL)(NSString*) = ^NSURL*(NSString *suffix) { + // TODO: use proper localized string + NSString *fileName = [[NSString stringWithFormat:@"%@ %@", NSLocalizedString(@"Default", @"default audio file name"), suffix] stringByAppendingPathExtension:@"aif"]; + if (!fileName) { + return nil; + } + NSString *filePath = [self.path stringByAppendingPathComponent:fileName]; + NSURL *fileURL = [NSURL fileURLWithPath:filePath]; + return fileURL; + }; + + NSURL *downFileURL = createOutURL(@"↓"); + NSURL *upFileURL = createOutURL(@"↑"); + NSAssert(downFileURL && upFileURL, @"Could not create out file URLs"); + if (!downFileURL || !upFileURL) { + os_log_error(OS_LOG_DEFAULT, "Could not create out file URLs"); + closeAudioFiles(); + return NO; + } + + OSStatus (^createOutFile)(NSURL*,AudioFileID*) = ^OSStatus(NSURL *fileURL, AudioFileID *audioFile) { + OSStatus result = AudioFileCreateWithURL((__bridge CFURLRef)fileURL, kAudioFileAIFFType, &asbd, kAudioFileFlags_EraseFile, audioFile); + + NSAssert1(noErr == result, @"Error setting absd for audio file %d", result); + if (result != noErr) { + os_log_error(OS_LOG_DEFAULT, "Error creating audio file at path %{public}@ %d", downFileURL.path, result); + + } else { + os_log_info(OS_LOG_DEFAULT, "Opened output audio file at path %{public}@", downFileURL.path); + } + return result; + }; + + OSStatus upResult = createOutFile(upFileURL, &upFile); + OSStatus downResult = createOutFile(downFileURL, &downFile); + + if (upResult != noErr || downResult != noErr) { + closeAudioFiles(); + return NO; + } + + NSMutableData *audioDataBuffer = [NSMutableData dataWithLength:totalBytes]; + UInt32 bytesToRead = (UInt32)totalBytes; + UInt32 packetsToRead = (UInt32)packetCount; + result = AudioFileReadPacketData(inAudioFile, false, &bytesToRead, NULL, 0, &packetsToRead, audioDataBuffer.mutableBytes); + NSAssert1(noErr == result, @"Error reading system audio file data %d", result); + audioDataBuffer.length = bytesToRead; + + UInt32 halfPackets = packetsToRead / 2; + UInt32 halfBytes = halfPackets * packetSize; + NSData *downData = [[audioDataBuffer subdataWithRange:NSMakeRange(0, halfBytes)] copy]; + NSData *upData = [[audioDataBuffer subdataWithRange:NSMakeRange(halfBytes, halfBytes)] copy]; + + + OSStatus (^writeOutFile)(NSData*,AudioFileID) = ^OSStatus(NSData *audioData, AudioFileID audioFile) { + UInt32 bytesToWrite = (UInt32)audioData.length; + UInt32 packetsToWrite = halfPackets; + OSStatus result = AudioFileWritePackets(audioFile, false, bytesToWrite, NULL, 0, &packetsToWrite, audioData.bytes); + NSAssert1(noErr == result, @"Error writing audio data %d", result); + result = AudioFileOptimize(audioFile); + return result; + }; + + result = writeOutFile(downData, downFile); + result = writeOutFile(upData, upFile); + + closeAudioFiles(); + + self.class.fingerUpFilePath = upFileURL.path; + self.class.fingerDownFilePath = downFileURL.path; + [self reloadPlayers]; + return YES; } + // MARK: - Class Properties + (NSString*) defaultPath { @@ -113,6 +245,14 @@ + (NSString*) defaultPath { return soundsDirectory; } + + +@end + +// MARK: - User Defaults + +@implementation HTKSounds (UserDefaults) + + (void) setFingerUpFilePath:(nullable NSString *)fingerUpFilePath { if (fingerUpFilePath) { [NSUserDefaults.standardUserDefaults setObject:fingerUpFilePath forKey:kFingerUpFilePathKey]; From 185c15af1bfa405f7f2152a2ae46744b99248649 Mon Sep 17 00:00:00 2001 From: Chris Ballinger Date: Fri, 2 Feb 2018 15:48:35 -0800 Subject: [PATCH 3/7] Check validity of source system sound file --- HapticKey/Classes/HTKSounds.h | 18 ++++-- HapticKey/Classes/HTKSounds.m | 112 +++++++++++++++++++++++----------- 2 files changed, 89 insertions(+), 41 deletions(-) diff --git a/HapticKey/Classes/HTKSounds.h b/HapticKey/Classes/HTKSounds.h index 2a01fd4..a6490d8 100644 --- a/HapticKey/Classes/HTKSounds.h +++ b/HapticKey/Classes/HTKSounds.h @@ -41,13 +41,20 @@ NS_ASSUME_NONNULL_BEGIN /** Play sound for finger down, if enabled */ - (void) playFingerDown; -/** Resets to use default sounds */ -- (BOOL) resetDefaultSounds; - // MARK: Class Properties +@end + +// MARK: - File Paths + +@interface HTKSounds (FilePaths) + /** ~/Library/Application Support/HapticKey/Sounds/ */ -@property (nonatomic, class, readonly) NSString *defaultPath; +@property (nonatomic, class, readonly) NSString *defaultSoundsDirectory; +/** Default sound for finger-up */ +@property (nonatomic, class, readonly) NSString *defaultUpFilePath; +/** Default sound for finger-down */ +@property (nonatomic, class, readonly) NSString *defaultDownFilePath; @end @@ -60,6 +67,9 @@ NS_ASSUME_NONNULL_BEGIN /** finger up sound file path stored in user defaults */ @property (nonatomic, class, nullable) NSString *fingerDownFilePath; +/** Force-resets to use default sounds */ +- (void) resetDefaultSounds; + @end NS_ASSUME_NONNULL_END diff --git a/HapticKey/Classes/HTKSounds.m b/HapticKey/Classes/HTKSounds.m index 019ac0b..087c792 100644 --- a/HapticKey/Classes/HTKSounds.m +++ b/HapticKey/Classes/HTKSounds.m @@ -14,11 +14,15 @@ NS_ASSUME_NONNULL_BEGIN -static NSString * const kDefaultSystemSoundFilePath = @"/System/Library/Components/CoreAudio.component/Contents/SharedSupport/SystemSounds/ink/InkSoundBecomeMouse.aif"; +static NSString * const kDefaultDownFileName = @"↓.aif"; +static NSString * const kDefaultUpFileName = @"↑.aif"; -//static NSString * const kDefaultSystemSoundFilePath = @"/System/Library/Components/CoreAudio.component/Contents/SharedSupport/SystemSounds/system/Media Keys.aif"; +static NSString * const kDefaultSystemSoundFilePath = @"/System/Library/Components/CoreAudio.component/Contents/SharedSupport/SystemSounds/ink/InkSoundBecomeMouse.aif"; +static UInt64 const kDefaultSystemSoundExpectedAudioBytes = 88032; +/** NSUserDefaults key */ static NSString * const kFingerUpFilePathKey = @"FingerUpFilePath"; +/** NSUserDefaults key */ static NSString * const kFingerDownFilePathKey = @"FingerDownFilePath"; @interface HTKSounds() @@ -41,16 +45,18 @@ - (nullable instancetype)initWithPath:(NSString *)path error:(NSError**)error NSParameterAssert(path); if (self = [super init]) { _path = [path copy]; - if (![self createDirectoryIfNeeded:error]) { + if (![self createSoundsDirectoryIfNeeded:error]) { return nil; } + [self createDefaultSoundsIfNeeded]; + [self checkValidityOfUserSoundPreferences]; [self reloadPlayers]; } return self; } - (nullable instancetype)initWithDefaultPath { - return [self initWithPath:self.class.defaultPath error:nil]; + return [self initWithPath:self.class.defaultSoundsDirectory error:nil]; } // MARK: - Public @@ -92,10 +98,21 @@ - (void) reloadPlayers { // MARK: - Private -- (BOOL) createDirectoryIfNeeded:(NSError**)error { - // create directory if it isn't already +/// checks if user's sound preferences point to valid paths +/// and unsets them if they are invalid +- (void) checkValidityOfUserSoundPreferences { + if (![NSFileManager.defaultManager fileExistsAtPath:self.class.fingerUpFilePath]) { + self.class.fingerUpFilePath = nil; + } + if (![NSFileManager.defaultManager fileExistsAtPath:self.class.fingerDownFilePath]) { + self.class.fingerDownFilePath = nil; + } +} + +/// creates destination sound directory if it is not present +- (BOOL) createSoundsDirectoryIfNeeded:(NSError**)error { BOOL isDirectory = NO; - BOOL exists = [NSFileManager.defaultManager fileExistsAtPath:_path isDirectory:&isDirectory]; + BOOL exists = [NSFileManager.defaultManager fileExistsAtPath:self.path isDirectory:&isDirectory]; if (exists && !isDirectory) { // path must be a directory return NO; @@ -106,8 +123,21 @@ - (BOOL) createDirectoryIfNeeded:(NSError**)error { } } -- (BOOL) resetDefaultSounds { - +/// on first launch, split the default system sound into two +/// separate sound files for finger-up and finger-down +/// returns YES if sounds are ready, or NO on failure +- (BOOL) createDefaultSoundsIfNeeded { + // bail out if we've got the files already + if ([NSFileManager.defaultManager fileExistsAtPath:self.class.defaultUpFilePath] && + [NSFileManager.defaultManager fileExistsAtPath:self.class.defaultDownFilePath]) { + return YES; + } else { + return [self createDefaultSoundFiles]; + } +} + +/// creates default up/down sound files from source system sound +- (BOOL) createDefaultSoundFiles { NSURL *originalFile = [NSURL fileURLWithPath:kDefaultSystemSoundFilePath]; AudioFileID inAudioFile = NULL; @@ -148,6 +178,15 @@ - (BOOL) resetDefaultSounds { specifierSize = sizeof(totalBytes); result = AudioFileGetProperty(inAudioFile, kAudioFilePropertyAudioDataByteCount, &specifierSize, &totalBytes); NSAssert1(noErr == result, @"Error getting audio byte count %d", result); + + // Check validity of source file by expected number of audio bytes + NSAssert2(kDefaultSystemSoundExpectedAudioBytes == totalBytes, @"Unexpected default system sound audio byte count! %llu vs %llu", totalBytes, kDefaultSystemSoundExpectedAudioBytes); + if (kDefaultSystemSoundExpectedAudioBytes != totalBytes) { + os_log_error(OS_LOG_DEFAULT, "Unexpected default system sound audio byte count! %llu vs %llu", totalBytes, kDefaultSystemSoundExpectedAudioBytes); + closeAudioFiles(); + return NO; + } + UInt64 packetCount = 0; specifierSize = sizeof(packetCount); result = AudioFileGetProperty(inAudioFile, kAudioFilePropertyAudioDataPacketCount, &specifierSize, &packetCount); @@ -157,26 +196,8 @@ - (BOOL) resetDefaultSounds { result = AudioFileGetProperty(inAudioFile, kAudioFilePropertyMaximumPacketSize, &specifierSize, &packetSize); NSAssert1(noErr == result, @"Error getting audio packet size %d", result); - - NSURL* (^createOutURL)(NSString*) = ^NSURL*(NSString *suffix) { - // TODO: use proper localized string - NSString *fileName = [[NSString stringWithFormat:@"%@ %@", NSLocalizedString(@"Default", @"default audio file name"), suffix] stringByAppendingPathExtension:@"aif"]; - if (!fileName) { - return nil; - } - NSString *filePath = [self.path stringByAppendingPathComponent:fileName]; - NSURL *fileURL = [NSURL fileURLWithPath:filePath]; - return fileURL; - }; - - NSURL *downFileURL = createOutURL(@"↓"); - NSURL *upFileURL = createOutURL(@"↑"); - NSAssert(downFileURL && upFileURL, @"Could not create out file URLs"); - if (!downFileURL || !upFileURL) { - os_log_error(OS_LOG_DEFAULT, "Could not create out file URLs"); - closeAudioFiles(); - return NO; - } + NSURL *downFileURL = [NSURL fileURLWithPath:self.class.defaultDownFilePath]; + NSURL *upFileURL = [NSURL fileURLWithPath:self.class.defaultUpFilePath]; OSStatus (^createOutFile)(NSURL*,AudioFileID*) = ^OSStatus(NSURL *fileURL, AudioFileID *audioFile) { OSStatus result = AudioFileCreateWithURL((__bridge CFURLRef)fileURL, kAudioFileAIFFType, &asbd, kAudioFileFlags_EraseFile, audioFile); @@ -226,17 +247,27 @@ - (BOOL) resetDefaultSounds { closeAudioFiles(); - self.class.fingerUpFilePath = upFileURL.path; - self.class.fingerDownFilePath = downFileURL.path; - [self reloadPlayers]; - return YES; } -// MARK: - Class Properties +@end + +// MARK: - File Paths + +@implementation HTKSounds (FilePaths) -+ (NSString*) defaultPath { ++ (NSString*) defaultUpFilePath { + NSString *filePath = [self.defaultSoundsDirectory stringByAppendingPathComponent:kDefaultUpFileName]; + return filePath; +} + ++ (NSString*) defaultDownFilePath { + NSString *filePath = [self.defaultSoundsDirectory stringByAppendingPathComponent:kDefaultDownFileName]; + return filePath; +} + ++ (NSString*) defaultSoundsDirectory { NSString *applicationSupportDirectory = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES).lastObject; NSString *applicationName = NSBundle.mainBundle.infoDictionary[(NSString*)kCFBundleNameKey]; NSString *appDirectory = [applicationSupportDirectory stringByAppendingPathComponent:applicationName]; @@ -245,10 +276,9 @@ + (NSString*) defaultPath { return soundsDirectory; } - - @end + // MARK: - User Defaults @implementation HTKSounds (UserDefaults) @@ -277,6 +307,14 @@ + (nullable NSString*) fingerDownFilePath { return [NSUserDefaults.standardUserDefaults stringForKey:kFingerDownFilePathKey]; } +- (void) resetDefaultSounds { + [self createDefaultSoundFiles]; + self.class.fingerUpFilePath = self.class.defaultUpFilePath; + self.class.fingerDownFilePath = self.class.defaultDownFilePath; + [self checkValidityOfUserSoundPreferences]; + [self reloadPlayers]; +} + @end NS_ASSUME_NONNULL_END From 09db86324e555be10b0b8c4412c86250ea4a700b Mon Sep 17 00:00:00 2001 From: Chris Ballinger Date: Fri, 2 Feb 2018 17:44:47 -0800 Subject: [PATCH 4/7] Add sound submenu items --- HapticKey.xcodeproj/project.pbxproj | 6 + HapticKey/Classes/HTKAppDelegate.m | 18 ++- HapticKey/Classes/HTKHapticFeedback.h | 5 +- HapticKey/Classes/HTKHapticFeedback.m | 5 +- HapticKey/Classes/HTKSoundMenu.h | 39 +++++ HapticKey/Classes/HTKSoundMenu.m | 203 ++++++++++++++++++++++++++ HapticKey/Classes/HTKSounds.m | 9 +- 7 files changed, 272 insertions(+), 13 deletions(-) create mode 100644 HapticKey/Classes/HTKSoundMenu.h create mode 100644 HapticKey/Classes/HTKSoundMenu.m diff --git a/HapticKey.xcodeproj/project.pbxproj b/HapticKey.xcodeproj/project.pbxproj index 910c7d9..8274b23 100644 --- a/HapticKey.xcodeproj/project.pbxproj +++ b/HapticKey.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 54FBCF321FD52483000EB4D3 /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 54FBCF301FD52483000EB4D3 /* AppIcon.icns */; }; 54FBCF331FD52483000EB4D3 /* StatusItem.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 54FBCF311FD52483000EB4D3 /* StatusItem.pdf */; }; D978A86E20218E8100F9810F /* HTKSounds.m in Sources */ = {isa = PBXBuildFile; fileRef = D978A86D20218E8100F9810F /* HTKSounds.m */; }; + D9F9D553202538C900E70D09 /* HTKSoundMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = D9F9D552202538C900E70D09 /* HTKSoundMenu.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -65,6 +66,8 @@ 54FBCF311FD52483000EB4D3 /* StatusItem.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = StatusItem.pdf; sourceTree = ""; }; D978A86C20218E8100F9810F /* HTKSounds.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HTKSounds.h; sourceTree = ""; }; D978A86D20218E8100F9810F /* HTKSounds.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HTKSounds.m; sourceTree = ""; }; + D9F9D551202538C900E70D09 /* HTKSoundMenu.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HTKSoundMenu.h; sourceTree = ""; }; + D9F9D552202538C900E70D09 /* HTKSoundMenu.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HTKSoundMenu.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -120,6 +123,8 @@ 54CDDBF11FE44F33005F35A1 /* Classes */ = { isa = PBXGroup; children = ( + D9F9D551202538C900E70D09 /* HTKSoundMenu.h */, + D9F9D552202538C900E70D09 /* HTKSoundMenu.m */, D978A86C20218E8100F9810F /* HTKSounds.h */, D978A86D20218E8100F9810F /* HTKSounds.m */, 548E15391FD00412001C0D4C /* HTKAppDelegate.h */, @@ -232,6 +237,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D9F9D553202538C900E70D09 /* HTKSoundMenu.m in Sources */, 5443CF0D1FE394BB002D4086 /* HTKHapticFeedback.m in Sources */, 549F9236200AF543003A8D7B /* HTKTimer.m in Sources */, 5443CEFF1FE38F6C002D4086 /* HTKEvent.m in Sources */, diff --git a/HapticKey/Classes/HTKAppDelegate.m b/HapticKey/Classes/HTKAppDelegate.m index e7b1423..f55badc 100644 --- a/HapticKey/Classes/HTKAppDelegate.m +++ b/HapticKey/Classes/HTKAppDelegate.m @@ -12,6 +12,7 @@ #import "HTKLoginItem.h" #import "HTKTapGestureEventListener.h" #import "HTKSounds.h" +#import "HTKSoundMenu.h" NS_ASSUME_NONNULL_BEGIN @@ -71,6 +72,9 @@ @interface HTKAppDelegate () @property (nonatomic, nullable) NSMenuItem *startOnLoginMenuItem; +@property (nonatomic, nullable) HTKSounds *sounds; +@property (nonatomic, nullable) HTKSoundMenu *soundMenu; + @end @implementation HTKAppDelegate @@ -202,7 +206,7 @@ - (void)_htk_main_updateHapticFeedback } if (eventListener) { - HTKHapticFeedback * const hapticFeedback = [[HTKHapticFeedback alloc] initWithEventListener:eventListener]; + HTKHapticFeedback * const hapticFeedback = [[HTKHapticFeedback alloc] initWithEventListener:eventListener sounds:self.sounds]; hapticFeedback.enabled = YES; self.hapticFeedback = hapticFeedback; } else { @@ -288,6 +292,7 @@ - (void)_htk_main_updateUserDefaults - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + [self _htk_main_loadSounds]; [self _htk_main_loadUserDefaults]; [self _htk_main_loadStatusItem]; [self _htk_main_loadMainBundleLoginItem]; @@ -300,6 +305,12 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification [self _htk_main_updateMainBundleLoginItem]; } +- (void)_htk_main_loadSounds { + HTKSounds *sounds = [[HTKSounds alloc] initWithDefaultPath]; + _sounds = sounds; + _soundMenu = [[HTKSoundMenu alloc] initWithSounds:sounds]; +} + - (void)_htk_main_loadUserDefaults { NSUserDefaults * const defaults = [NSUserDefaults standardUserDefaults]; @@ -420,8 +431,9 @@ - (void)_htk_main_loadStatusItem NSMenuItem * const useSoundEffectMenuItem = [[NSMenuItem alloc] init]; useSoundEffectMenuItem.title = NSLocalizedString(@"STATUS_MENU_ITEM_SOUND_EFFECT_MENU_ITEM", @"A status menu item to use sound effect."); - useSoundEffectMenuItem.action = @selector(_htk_action_didSelectSoundEffectTypeMenuItem:); - useSoundEffectMenuItem.target = self; + //useSoundEffectMenuItem.action = @selector(_htk_action_didSelectSoundEffectTypeMenuItem:); + //useSoundEffectMenuItem.target = self; + useSoundEffectMenuItem.submenu = self.soundMenu.soundSubmenu; [statusMenu addItem:useSoundEffectMenuItem]; self.useSoundEffectMenuItem = useSoundEffectMenuItem; diff --git a/HapticKey/Classes/HTKHapticFeedback.h b/HapticKey/Classes/HTKHapticFeedback.h index abd87d5..4fe6fb5 100644 --- a/HapticKey/Classes/HTKHapticFeedback.h +++ b/HapticKey/Classes/HTKHapticFeedback.h @@ -31,13 +31,14 @@ typedef NS_ENUM(NSUInteger, HTKSoundFeedbackType) { @property (nonatomic, getter=isEnabled) BOOL enabled; @property (nonatomic) HTKHapticFeedbackType type; @property (nonatomic) HTKSoundFeedbackType soundType; -@property (nonatomic, readonly) HTKSounds *sounds; +@property (nonatomic, readonly, nullable) HTKSounds *sounds; @property (nonatomic, getter=isScreenFlashEnabled) BOOL screenFlashEnabled; + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithEventListener:(HTKEventListener *)eventListener NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithEventListener:(HTKEventListener *)eventListener + sounds:(nullable HTKSounds*)sounds NS_DESIGNATED_INITIALIZER; @end diff --git a/HapticKey/Classes/HTKHapticFeedback.m b/HapticKey/Classes/HTKHapticFeedback.m index 277a6ae..42bee5c 100644 --- a/HapticKey/Classes/HTKHapticFeedback.m +++ b/HapticKey/Classes/HTKHapticFeedback.m @@ -38,14 +38,13 @@ - (instancetype)init } - (instancetype)initWithEventListener:(HTKEventListener *)eventListener + sounds:(nullable HTKSounds*)sounds { if (self = [super init]) { _eventListener = eventListener; _eventListener.delegate = self; _type = HTKHapticFeedbackTypeMedium; - - _sounds = [[HTKSounds alloc] initWithDefaultPath]; - [_sounds resetDefaultSounds]; + _sounds = sounds; } return self; } diff --git a/HapticKey/Classes/HTKSoundMenu.h b/HapticKey/Classes/HTKSoundMenu.h new file mode 100644 index 0000000..9ff3ecd --- /dev/null +++ b/HapticKey/Classes/HTKSoundMenu.h @@ -0,0 +1,39 @@ +// +// HTKSoundMenu.h +// HapticKey +// +// Created by Chris Ballinger on 2/2/18. +// Copyright © 2018 Yoshimasa Niwa. All rights reserved. +// + +@import Foundation; +@import AppKit; + +@class HTKSounds; + +NS_ASSUME_NONNULL_BEGIN + +/** + * This class helps create and manage the sound submenu. + */ +@interface HTKSoundMenu : NSObject + +// MARK: Properties + +@property (nonatomic, readonly) HTKSounds *sounds; +@property (nonatomic, readonly) NSMenu *soundSubmenu; + +// MARK: Init + ++ (instancetype)new NS_UNAVAILABLE; +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithSounds:(HTKSounds *)sounds NS_DESIGNATED_INITIALIZER; + +// MARK: Public Methods + +- (void) refreshMenuItems; + +@end + +NS_ASSUME_NONNULL_END diff --git a/HapticKey/Classes/HTKSoundMenu.m b/HapticKey/Classes/HTKSoundMenu.m new file mode 100644 index 0000000..1a8df0c --- /dev/null +++ b/HapticKey/Classes/HTKSoundMenu.m @@ -0,0 +1,203 @@ +// +// HTKSoundMenu.m +// HapticKey +// +// Created by Chris Ballinger on 2/2/18. +// Copyright © 2018 Yoshimasa Niwa. All rights reserved. +// + +#import "HTKSoundMenu.h" +#import "HTKSounds.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, HTKFingerDirection) { + HTKFingerDirectionUp, + HTKFingerDirectionDown +}; + +@interface HTKSoundMenu() + +/// all menu items in soundSubmenu +//@property (nonatomic, readonly) NSArray *allMenuItems; +/// menu items corresponding to sound files on disk +@property (nonatomic, readonly) NSArray *soundFileMenuItems; + +@end + +@implementation HTKSoundMenu + +// MARK: - Init + +- (instancetype)init +{ + [self doesNotRecognizeSelector:_cmd]; + abort(); +} + +- (instancetype)initWithSounds:(HTKSounds *)sounds +{ + NSParameterAssert(sounds); + if (self = [super init]) { + _sounds = sounds; + _soundSubmenu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Sound", @"label for sound submenu")]; + [self refreshMenuItems]; + } + return self; +} + +// MARK: Public Methods + +- (void) refreshMenuItems { + [self.soundSubmenu removeAllItems]; + [self addSoundSubmenuItems]; + +} + +// MARK: UI Actions + +- (void) openSoundDirectoryInFinder:(NSMenuItem*)sender { + [NSWorkspace.sharedWorkspace openURL:[NSURL fileURLWithPath:self.sounds.path]]; +} + +- (void) soundItemSelected:(NSMenuItem*)sender { + HTKFingerDirection fingerDirection = sender.tag; + NSString *soundFilePath = sender.representedObject; + + switch (fingerDirection) { + case HTKFingerDirectionUp: + HTKSounds.fingerUpFilePath = soundFilePath; + break; + case HTKFingerDirectionDown: + HTKSounds.fingerDownFilePath = soundFilePath; + break; + } + [self updateStateForSoundMenuItems]; + [self.sounds reloadPlayers]; +} + +// MARK: Private Methods + +/// adds/removes checkmarks to the sound menu items that were chosen by the user +- (void) updateStateForSoundMenuItems { + [self.soundFileMenuItems enumerateObjectsUsingBlock:^(NSMenuItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [self setStateForSoundMenuItem:obj]; + }]; +} + +- (NSMenuItem *) menuItemForSoundFilePath:(nullable NSString*)soundFilePath fingerDirection:(HTKFingerDirection)fingerDirection { + NSMenuItem *menuItem = [[NSMenuItem alloc] init]; + menuItem.representedObject = soundFilePath; + menuItem.tag = fingerDirection; + + NSString *title = soundFilePath.lastPathComponent.stringByDeletingPathExtension; + if (!title) { + title = NSLocalizedString(@"None", "no sound menu item selected"); + } + + if ([soundFilePath isEqualToString:HTKSounds.defaultUpFilePath] || + [soundFilePath isEqualToString:HTKSounds.defaultDownFilePath]) { + NSString *defaultString = NSLocalizedString(@"Default", @"string for default sound"); + title = [NSString stringWithFormat:@"%@ %@", defaultString, title]; + } + menuItem.title = title; + + [self setStateForSoundMenuItem:menuItem]; + + menuItem.target = self; + menuItem.action = @selector(soundItemSelected:); + return menuItem; +} + +- (void) setStateForSoundMenuItem:(NSMenuItem*)menuItem { + NSString *soundFilePath = menuItem.representedObject; + HTKFingerDirection direction = menuItem.tag; + + NSString *upPath = HTKSounds.fingerUpFilePath; + NSString *downPath = HTKSounds.fingerDownFilePath; + + switch (direction) { + case HTKFingerDirectionUp: + if ((soundFilePath && upPath && [soundFilePath isEqualToString:HTKSounds.fingerUpFilePath]) || + (soundFilePath == nil && upPath == nil) ) { + menuItem.state = NSOnState; + } else { + menuItem.state = NSOffState; + } + break; + case HTKFingerDirectionDown: + if ((soundFilePath && downPath && [soundFilePath isEqualToString:downPath]) || + (soundFilePath == nil && downPath == nil)) { + menuItem.state = NSOnState; + } else { + menuItem.state = NSOffState; + + } + break; + } +} + +- (void) addSoundSubmenuItems { + NSArray *menuItems = self.allMenuItems; + [menuItems enumerateObjectsUsingBlock:^(NSMenuItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [self.soundSubmenu addItem:obj]; + }]; +} + +- (NSArray*) generateSoundMenuItemsForDirection:(HTKFingerDirection)direction { + NSMutableArray* items = [NSMutableArray array]; + [self.sounds.allSoundFiles enumerateObjectsUsingBlock:^(NSURL * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + NSMenuItem *item = [self menuItemForSoundFilePath:obj.path fingerDirection:direction]; + [items addObject:item]; + }]; + NSMenuItem *noneItem = [self menuItemForSoundFilePath:nil fingerDirection:direction]; + [items addObject:noneItem]; + return items; +} + +- (NSArray*) allMenuItems { + + + NSMutableArray *menuItems = [NSMutableArray array]; + + // Finger Down + + NSMenuItem *fingerDownLabel = [[NSMenuItem alloc] init]; + fingerDownLabel.title = NSLocalizedString(@"Finger Down", @"section label for finger down settings"); + fingerDownLabel.enabled = NO; + [menuItems addObject:fingerDownLabel]; + + NSArray *downItems = [self generateSoundMenuItemsForDirection:HTKFingerDirectionDown]; + [menuItems addObjectsFromArray:downItems]; + + // Finger Up + [menuItems addObject:NSMenuItem.separatorItem]; + + NSMenuItem *fingerUpLabel = [[NSMenuItem alloc] init]; + fingerUpLabel.title = NSLocalizedString(@"Finger Up", @"section label for finger up settings"); + fingerDownLabel.enabled = NO; + [menuItems addObject:fingerUpLabel]; + + NSArray *upItems = [self generateSoundMenuItemsForDirection:HTKFingerDirectionUp]; + [menuItems addObjectsFromArray:upItems]; + + // Add Sounds... + BOOL showCustom = YES; + if (showCustom) { + [menuItems addObject:NSMenuItem.separatorItem]; + + NSMenuItem *addSounds = [[NSMenuItem alloc] init]; + addSounds.title = NSLocalizedString(@"Add Sounds...", @"menu item for adding custom sounds"); + addSounds.action = @selector(openSoundDirectoryInFinder:); + addSounds.target = self; + [menuItems addObject:addSounds]; + } + + _soundFileMenuItems = [upItems arrayByAddingObjectsFromArray:downItems]; + + return menuItems; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/HapticKey/Classes/HTKSounds.m b/HapticKey/Classes/HTKSounds.m index 087c792..e6ae49b 100644 --- a/HapticKey/Classes/HTKSounds.m +++ b/HapticKey/Classes/HTKSounds.m @@ -14,8 +14,8 @@ NS_ASSUME_NONNULL_BEGIN -static NSString * const kDefaultDownFileName = @"↓.aif"; -static NSString * const kDefaultUpFileName = @"↑.aif"; +static NSString * const kDefaultDownSoundFileName = @"↓.aif"; +static NSString * const kDefaultUpSoundFileName = @"↑.aif"; static NSString * const kDefaultSystemSoundFilePath = @"/System/Library/Components/CoreAudio.component/Contents/SharedSupport/SystemSounds/ink/InkSoundBecomeMouse.aif"; static UInt64 const kDefaultSystemSoundExpectedAudioBytes = 88032; @@ -250,7 +250,6 @@ - (BOOL) createDefaultSoundFiles { return YES; } - @end // MARK: - File Paths @@ -258,12 +257,12 @@ - (BOOL) createDefaultSoundFiles { @implementation HTKSounds (FilePaths) + (NSString*) defaultUpFilePath { - NSString *filePath = [self.defaultSoundsDirectory stringByAppendingPathComponent:kDefaultUpFileName]; + NSString *filePath = [self.defaultSoundsDirectory stringByAppendingPathComponent:kDefaultUpSoundFileName]; return filePath; } + (NSString*) defaultDownFilePath { - NSString *filePath = [self.defaultSoundsDirectory stringByAppendingPathComponent:kDefaultDownFileName]; + NSString *filePath = [self.defaultSoundsDirectory stringByAppendingPathComponent:kDefaultDownSoundFileName]; return filePath; } From c16039545214ba095cdd867307341912df289752 Mon Sep 17 00:00:00 2001 From: Chris Ballinger Date: Sat, 3 Feb 2018 13:10:16 -0800 Subject: [PATCH 5/7] Add volume customization --- HapticKey/Classes/HTKSoundMenu.m | 81 ++++++++++++++++++++++++++++++-- HapticKey/Classes/HTKSounds.h | 7 +++ HapticKey/Classes/HTKSounds.m | 55 +++++++++++++++++++--- 3 files changed, 134 insertions(+), 9 deletions(-) diff --git a/HapticKey/Classes/HTKSoundMenu.m b/HapticKey/Classes/HTKSoundMenu.m index 1a8df0c..c65aab3 100644 --- a/HapticKey/Classes/HTKSoundMenu.m +++ b/HapticKey/Classes/HTKSoundMenu.m @@ -16,12 +16,19 @@ typedef NS_ENUM(NSInteger, HTKFingerDirection) { HTKFingerDirectionDown }; +@interface NSView (Constranits) +- (void)htk_pinToView:(NSView *)view edge:(NSLayoutAttribute)edge offset:(CGFloat)offset; +- (void)htk_pinToView:(NSView *)view edge:(NSLayoutAttribute)attribute; +- (void)htk_pinAllEdgesToView:(NSView *)view; +@end + @interface HTKSoundMenu() /// all menu items in soundSubmenu //@property (nonatomic, readonly) NSArray *allMenuItems; /// menu items corresponding to sound files on disk @property (nonatomic, readonly) NSArray *soundFileMenuItems; +@property (nonatomic, readonly) NSSlider *volumeSlider; @end @@ -41,6 +48,8 @@ - (instancetype)initWithSounds:(HTKSounds *)sounds if (self = [super init]) { _sounds = sounds; _soundSubmenu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Sound", @"label for sound submenu")]; + _volumeSlider = [NSSlider sliderWithTarget:self action:@selector(volumeSliderValueChanged:)]; + _volumeSlider.translatesAutoresizingMaskIntoConstraints = NO; [self refreshMenuItems]; } return self; @@ -51,7 +60,7 @@ - (instancetype)initWithSounds:(HTKSounds *)sounds - (void) refreshMenuItems { [self.soundSubmenu removeAllItems]; [self addSoundSubmenuItems]; - + [self updateVolumeSlider]; } // MARK: UI Actions @@ -67,17 +76,36 @@ - (void) soundItemSelected:(NSMenuItem*)sender { switch (fingerDirection) { case HTKFingerDirectionUp: HTKSounds.fingerUpFilePath = soundFilePath; + [self.sounds reloadFingerUp]; + [self.sounds playFingerUp]; break; case HTKFingerDirectionDown: HTKSounds.fingerDownFilePath = soundFilePath; + [self.sounds reloadFingerDown]; + [self.sounds playFingerDown]; break; } [self updateStateForSoundMenuItems]; - [self.sounds reloadPlayers]; +} + +- (void) volumeSliderValueChanged:(NSSlider*)sender { + HTKSounds.desiredVolume = sender.floatValue; + [self.sounds updateVolume]; + // preview volume level + if (HTKSounds.fingerDownFilePath) { + [self.sounds playFingerDown]; + } else if (HTKSounds.fingerUpFilePath) { + [self.sounds playFingerUp]; + } } // MARK: Private Methods +- (void) updateVolumeSlider { + float value = HTKSounds.desiredVolume; + self.volumeSlider.floatValue = value; +} + /// adds/removes checkmarks to the sound menu items that were chosen by the user - (void) updateStateForSoundMenuItems { [self.soundFileMenuItems enumerateObjectsUsingBlock:^(NSMenuItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { @@ -175,12 +203,30 @@ - (void) addSoundSubmenuItems { NSMenuItem *fingerUpLabel = [[NSMenuItem alloc] init]; fingerUpLabel.title = NSLocalizedString(@"Finger Up", @"section label for finger up settings"); - fingerDownLabel.enabled = NO; + fingerUpLabel.enabled = NO; [menuItems addObject:fingerUpLabel]; NSArray *upItems = [self generateSoundMenuItemsForDirection:HTKFingerDirectionUp]; [menuItems addObjectsFromArray:upItems]; + // Volume Control + [menuItems addObject:NSMenuItem.separatorItem]; + + NSMenuItem *volumeLabel = [[NSMenuItem alloc] init]; + volumeLabel.title = NSLocalizedString(@"Volume", @"section label for sound effect volume setting"); + volumeLabel.enabled = NO; + [menuItems addObject:volumeLabel]; + + NSMenuItem *volumeItem = [[NSMenuItem alloc] init]; + NSView *view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 100, 19)]; + [view addSubview:self.volumeSlider]; + [view htk_pinToView:self.volumeSlider edge:NSLayoutAttributeLeading offset:-20]; + [view htk_pinToView:self.volumeSlider edge:NSLayoutAttributeTop offset:0]; + [view htk_pinToView:self.volumeSlider edge:NSLayoutAttributeBottom offset:0]; + [view htk_pinToView:self.volumeSlider edge:NSLayoutAttributeTrailing offset:0]; + volumeItem.view = view; + [menuItems addObject:volumeItem]; + // Add Sounds... BOOL showCustom = YES; if (showCustom) { @@ -193,6 +239,7 @@ - (void) addSoundSubmenuItems { [menuItems addObject:addSounds]; } + // store sounds separately so we can toggle them _soundFileMenuItems = [upItems arrayByAddingObjectsFromArray:downItems]; return menuItems; @@ -200,4 +247,32 @@ - (void) addSoundSubmenuItems { @end +// MARK: - View Constraints + +@implementation NSView (Constranits) + +- (void)htk_pinToView:(NSView *)view edge:(NSLayoutAttribute)edge { + [self htk_pinToView:view edge:edge offset:0.0]; +} + +- (void)htk_pinToView:(NSView *)view edge:(NSLayoutAttribute)edge offset:(CGFloat)offset +{ + [self addConstraint:[NSLayoutConstraint constraintWithItem:self + attribute:edge + relatedBy:NSLayoutRelationEqual + toItem:view + attribute:edge + multiplier:1.0f + constant:offset]]; +} + +- (void)htk_pinAllEdgesToView:(NSView *)view +{ + [self htk_pinToView:view edge:NSLayoutAttributeBottom]; + [self htk_pinToView:view edge:NSLayoutAttributeTop]; + [self htk_pinToView:view edge:NSLayoutAttributeLeading]; + [self htk_pinToView:view edge:NSLayoutAttributeTrailing]; +} +@end + NS_ASSUME_NONNULL_END diff --git a/HapticKey/Classes/HTKSounds.h b/HapticKey/Classes/HTKSounds.h index a6490d8..2c34d93 100644 --- a/HapticKey/Classes/HTKSounds.h +++ b/HapticKey/Classes/HTKSounds.h @@ -35,6 +35,10 @@ NS_ASSUME_NONNULL_BEGIN /** Reloads fingerUp and fingerDown players from user preferences */ - (void) reloadPlayers; +- (void) reloadFingerUp; +- (void) reloadFingerDown; +/** Updates player volumes based on stored user preference */ +- (void) updateVolume; /** Play sound for finger up, if enabled */ - (void) playFingerUp; @@ -67,6 +71,9 @@ NS_ASSUME_NONNULL_BEGIN /** finger up sound file path stored in user defaults */ @property (nonatomic, class, nullable) NSString *fingerDownFilePath; +/** volume from 0.0 -> 1.0. Takes effect after reloadPlayers is called. */ +@property (nonatomic, class) float desiredVolume; + /** Force-resets to use default sounds */ - (void) resetDefaultSounds; diff --git a/HapticKey/Classes/HTKSounds.m b/HapticKey/Classes/HTKSounds.m index e6ae49b..11109cf 100644 --- a/HapticKey/Classes/HTKSounds.m +++ b/HapticKey/Classes/HTKSounds.m @@ -24,6 +24,8 @@ static NSString * const kFingerUpFilePathKey = @"FingerUpFilePath"; /** NSUserDefaults key */ static NSString * const kFingerDownFilePathKey = @"FingerDownFilePath"; +static NSString * const kDesiredVolumeKey = @"DesiredVolume"; + @interface HTKSounds() @property (nonatomic, readonly, nullable) AVAudioPlayer *fingerUp; @@ -70,20 +72,33 @@ - (void) playFingerDown { } - (void) reloadPlayers { + [self reloadFingerDown]; + [self reloadFingerUp]; +} + +- (void) reloadFingerUp { _fingerUp = nil; - _fingerDown = nil; - NSError *error = nil; NSString *fingerUpFilePath = self.class.fingerUpFilePath; - NSString *fingerDownFilePath = self.class.fingerDownFilePath; - if (fingerUpFilePath) { NSURL *fileURL = [NSURL fileURLWithPath:fingerUpFilePath]; - _fingerUp = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&error]; + _fingerUp = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:nil]; } + _fingerUp.volume = self.class.desiredVolume; +} + +- (void) reloadFingerDown { + _fingerDown = nil; + NSString *fingerDownFilePath = self.class.fingerDownFilePath; if (fingerDownFilePath) { NSURL *fileURL = [NSURL fileURLWithPath:fingerDownFilePath]; - _fingerDown = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&error]; + _fingerDown = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:nil]; } + _fingerDown.volume = self.class.desiredVolume; +} + +- (void) updateVolume { + _fingerUp.volume = self.class.desiredVolume; + _fingerDown.volume = self.class.desiredVolume; } - (NSArray*) allSoundFiles { @@ -288,6 +303,7 @@ + (void) setFingerUpFilePath:(nullable NSString *)fingerUpFilePath { } else { [NSUserDefaults.standardUserDefaults removeObjectForKey:kFingerUpFilePathKey]; } + [NSUserDefaults.standardUserDefaults synchronize]; } + (nullable NSString*) fingerUpFilePath { @@ -300,12 +316,39 @@ + (void) setFingerDownFilePath:(nullable NSString *)fingerDownFilePath { } else { [NSUserDefaults.standardUserDefaults removeObjectForKey:kFingerDownFilePathKey]; } + [NSUserDefaults.standardUserDefaults synchronize]; } + (nullable NSString*) fingerDownFilePath { return [NSUserDefaults.standardUserDefaults stringForKey:kFingerDownFilePathKey]; } +/// returns valid volume range within 0.0->1.0 ++ (float) validVolume:(float)value { + if (value > 1.0) { + return 1.0; + } else if (value < 0.0) { + return 0.0; + } else { + return value; + } +} + ++ (float) desiredVolume { + // default to 1.0 if unset + if(![NSUserDefaults.standardUserDefaults objectForKey:kDesiredVolumeKey]){ + return 1.0; + } + float value = [NSUserDefaults.standardUserDefaults floatForKey:kDesiredVolumeKey]; + return [self validVolume:value]; +} + ++ (void) setDesiredVolume:(float)desiredVolume { + float value = [self validVolume:desiredVolume]; + [NSUserDefaults.standardUserDefaults setFloat:value forKey:kDesiredVolumeKey]; + [NSUserDefaults.standardUserDefaults synchronize]; +} + - (void) resetDefaultSounds { [self createDefaultSoundFiles]; self.class.fingerUpFilePath = self.class.defaultUpFilePath; From 0a36b1395a112f921c0edfb7aae7292eb228a9b7 Mon Sep 17 00:00:00 2001 From: Chris Ballinger Date: Sat, 3 Feb 2018 13:19:33 -0800 Subject: [PATCH 6/7] Dynamically refresh sound file menu items --- HapticKey/Classes/HTKSoundMenu.m | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/HapticKey/Classes/HTKSoundMenu.m b/HapticKey/Classes/HTKSoundMenu.m index c65aab3..9b902b2 100644 --- a/HapticKey/Classes/HTKSoundMenu.m +++ b/HapticKey/Classes/HTKSoundMenu.m @@ -22,7 +22,7 @@ - (void)htk_pinToView:(NSView *)view edge:(NSLayoutAttribute)attribute; - (void)htk_pinAllEdgesToView:(NSView *)view; @end -@interface HTKSoundMenu() +@interface HTKSoundMenu() /// all menu items in soundSubmenu //@property (nonatomic, readonly) NSArray *allMenuItems; @@ -48,6 +48,7 @@ - (instancetype)initWithSounds:(HTKSounds *)sounds if (self = [super init]) { _sounds = sounds; _soundSubmenu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Sound", @"label for sound submenu")]; + _soundSubmenu.delegate = self; _volumeSlider = [NSSlider sliderWithTarget:self action:@selector(volumeSliderValueChanged:)]; _volumeSlider.translatesAutoresizingMaskIntoConstraints = NO; [self refreshMenuItems]; @@ -217,6 +218,7 @@ - (void) addSoundSubmenuItems { volumeLabel.enabled = NO; [menuItems addObject:volumeLabel]; + // Can't seem to get the volume slider's left inset to work without doing this NSMenuItem *volumeItem = [[NSMenuItem alloc] init]; NSView *view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 100, 19)]; [view addSubview:self.volumeSlider]; @@ -245,6 +247,12 @@ - (void) addSoundSubmenuItems { return menuItems; } +// MARK: - NSMenuDelegate + +- (void) menuNeedsUpdate:(NSMenu *)menu { + [self refreshMenuItems]; +} + @end // MARK: - View Constraints From 5063d96d424e1a47016a06aaf8eee9a6d4277e7d Mon Sep 17 00:00:00 2001 From: Chris Ballinger Date: Sat, 3 Feb 2018 13:29:26 -0800 Subject: [PATCH 7/7] Match project localized string format --- HapticKey/Base.lproj/Localizable.strings | Bin 3054 -> 4258 bytes HapticKey/Classes/HTKSoundMenu.m | 29 ++++++++++------------- HapticKey/en.lproj/Localizable.strings | Bin 2444 -> 3466 bytes 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/HapticKey/Base.lproj/Localizable.strings b/HapticKey/Base.lproj/Localizable.strings index 64eae85e502cfd95af691c825428421735d795f1..212251ce97ab921d72afc652b2b3836c6d8c04f6 100644 GIT binary patch literal 4258 zcmb`KTW`}q5QXQtzryNcALtK2JhaUP)DR$-R(*yxypC zy}S0V_nbX5bMouwrCnHJS6mz0+L&ucE3t2u+KP5!3&v9VdLy+Z*VIR5jO^{v-^<_9 zf6blweQXndduDGKyRb_;vvcd)fUgZ~NNZ#tt!Z7_Bm2Nulm5sWwEMhsZ5>7$2hYw~ z=N(t||LfF6 zU!491H7KvB#-wCesX7#NQ0U5s zj2v_J8F8})@0g4$Z<5E*)T^<)Z|?q!oGg!D`uQ@)zRnmUG}7?=TjtC@{=M|^b!H!; zkt0`SDBP#K_=K^gpI(mYU)3bRp+51ZoLKJbQ&!SiIsU2`@5mG#_h8KcY=6!Q#gx75 zQf@)6NwF<1%Ke9&QpeV`L)74e?x|{ZQI+#Uk3vm5QBWTc2&f7)nEQq_bkucW#84AFK1|< FzX5YkjH>_u delta 7 OcmZ3a_)dJoJ8l3DGy`$~ diff --git a/HapticKey/Classes/HTKSoundMenu.m b/HapticKey/Classes/HTKSoundMenu.m index 9b902b2..b2141d9 100644 --- a/HapticKey/Classes/HTKSoundMenu.m +++ b/HapticKey/Classes/HTKSoundMenu.m @@ -47,7 +47,7 @@ - (instancetype)initWithSounds:(HTKSounds *)sounds NSParameterAssert(sounds); if (self = [super init]) { _sounds = sounds; - _soundSubmenu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Sound", @"label for sound submenu")]; + _soundSubmenu = [[NSMenu alloc] init]; _soundSubmenu.delegate = self; _volumeSlider = [NSSlider sliderWithTarget:self action:@selector(volumeSliderValueChanged:)]; _volumeSlider.translatesAutoresizingMaskIntoConstraints = NO; @@ -121,12 +121,12 @@ - (NSMenuItem *) menuItemForSoundFilePath:(nullable NSString*)soundFilePath fing NSString *title = soundFilePath.lastPathComponent.stringByDeletingPathExtension; if (!title) { - title = NSLocalizedString(@"None", "no sound menu item selected"); + title = NSLocalizedString(@"SOUND_MENU_ITEM_NONE", "no sound menu item selected"); } if ([soundFilePath isEqualToString:HTKSounds.defaultUpFilePath] || [soundFilePath isEqualToString:HTKSounds.defaultDownFilePath]) { - NSString *defaultString = NSLocalizedString(@"Default", @"string for default sound"); + NSString *defaultString = NSLocalizedString(@"SOUND_MENU_ITEM_DEFAULT", @"section label string for default selection"); title = [NSString stringWithFormat:@"%@ %@", defaultString, title]; } menuItem.title = title; @@ -192,7 +192,7 @@ - (void) addSoundSubmenuItems { // Finger Down NSMenuItem *fingerDownLabel = [[NSMenuItem alloc] init]; - fingerDownLabel.title = NSLocalizedString(@"Finger Down", @"section label for finger down settings"); + fingerDownLabel.title = NSLocalizedString(@"SOUND_MENU_ITEM_FINGER_DOWN", @"section label for finger down settings"); fingerDownLabel.enabled = NO; [menuItems addObject:fingerDownLabel]; @@ -203,7 +203,7 @@ - (void) addSoundSubmenuItems { [menuItems addObject:NSMenuItem.separatorItem]; NSMenuItem *fingerUpLabel = [[NSMenuItem alloc] init]; - fingerUpLabel.title = NSLocalizedString(@"Finger Up", @"section label for finger up settings"); + fingerUpLabel.title = NSLocalizedString(@"SOUND_MENU_ITEM_FINGER_UP", @"section label for finger up settings"); fingerUpLabel.enabled = NO; [menuItems addObject:fingerUpLabel]; @@ -214,7 +214,7 @@ - (void) addSoundSubmenuItems { [menuItems addObject:NSMenuItem.separatorItem]; NSMenuItem *volumeLabel = [[NSMenuItem alloc] init]; - volumeLabel.title = NSLocalizedString(@"Volume", @"section label for sound effect volume setting"); + volumeLabel.title = NSLocalizedString(@"SOUND_MENU_ITEM_VOLUME", @"section label for sound effect volume setting"); volumeLabel.enabled = NO; [menuItems addObject:volumeLabel]; @@ -230,16 +230,13 @@ - (void) addSoundSubmenuItems { [menuItems addObject:volumeItem]; // Add Sounds... - BOOL showCustom = YES; - if (showCustom) { - [menuItems addObject:NSMenuItem.separatorItem]; - - NSMenuItem *addSounds = [[NSMenuItem alloc] init]; - addSounds.title = NSLocalizedString(@"Add Sounds...", @"menu item for adding custom sounds"); - addSounds.action = @selector(openSoundDirectoryInFinder:); - addSounds.target = self; - [menuItems addObject:addSounds]; - } + [menuItems addObject:NSMenuItem.separatorItem]; + + NSMenuItem *addSounds = [[NSMenuItem alloc] init]; + addSounds.title = NSLocalizedString(@"SOUND_MENU_ITEM_ADD_SOUNDS", @"menu item for adding custom sounds"); + addSounds.action = @selector(openSoundDirectoryInFinder:); + addSounds.target = self; + [menuItems addObject:addSounds]; // store sounds separately so we can toggle them _soundFileMenuItems = [upItems arrayByAddingObjectsFromArray:downItems]; diff --git a/HapticKey/en.lproj/Localizable.strings b/HapticKey/en.lproj/Localizable.strings index 3eba7dba7ae97736444e547170983389015b3f38..9b86844b215adf007bd86611e75744929ce100db 100644 GIT binary patch literal 3466 zcmb`KOHbQS5QS&WuduR26{y`-T?A~?ijbB#3F;~ei36yVpu{QapKtq}af}`DisK3y zUtgO$bLO0R@b}N5?OR}NuBBx*=321|?5Cx6$2zbZ_EMhpMQXQP6W^J#^I+HhUH+AR z>u;xiAKRsWJ+%Y&_U+L2tYwMyvDUW%tC5{qWL?%H`@&wt^TN>VmTmPhTD9DR{qwF|wf} z@4taJA|}W9FvcUPD7N?Z&hH=mo}#abdv)sGE@8YWztXdGxpF1Xc+&GFK1_&7y$Fa) zsdV6ajd{}Z5nLbJ=^NP3Dt_#a>JBzDB37uk8J;dYO09KpbX`wr17=iJ7O-)H3RHKL zg_+kADdV1>Ti#9lU9R4GESuz^`3SO2(Nf{`Wr&Ja3 zr0Q7@NdiC0In}}lp2U>$?{#EJ#dcJ&y*sMBLo7U6&odObDgv0B+m$n07pb~K&#~pT zr33a8cvL-oy@slL<-X;WuJh=_?ubhsGtSnzS1{Qu+tdMjUO7*|R4?liVqf^t9F-z= z51uPjDQ;Jstt{NVH;BD&pgKXJ!a4A6xP516qnukj=(}f{C2}tN+*f4IH9JSnb93|$ z;2cx?oAP<#byIb}9+wwu`&A^GUY?u0X5e4&ZcHUn7t3`xAtvH&zMjsCTBEp?^Hy0q zp61U8zCy1nWsOOw>BXGYc6$L_mS-POy$&|&zP#2HrOH;!8JeFi*%O}ssH^Le)RLcL1;R{0{VrGIsz9eX8HcJ$E8 zfLtkK9dB3~V5zCMQ;gst}WL0d`e7bXl8{D)QzkEYZRK=RtF*R rjZyXhHS{aKh8s3EebhuHtA);=&%>%$njg2%GtF3C{3;{2IYIpcRec%i delta 7 OcmeB@?h)S5!wCQir~--r