2 * \file AngbandAudio.mm
3 * \brief Define an interface for handling incidental sounds and background
4 * music in the OS X front end.
7 #import <Cocoa/Cocoa.h>
8 #import "AngbandAudio.h"
9 #include "io/files-util.h"
10 #include "main/music-definitions-table.h"
11 #include "main/sound-definitions-table.h"
12 #include "main/sound-of-music.h"
13 #include "util/angband-files.h"
20 * Helper functions to extract the appropriate number ID from a name in
21 * music.cfg. Return true if the extraction failed and false if the extraction
22 * succeeded with *id holding the result.
24 static bool get_basic_id(const char *s, int *id)
29 if (i >= MUSIC_BASIC_MAX) {
32 if (!strcmp(angband_music_basic_name[i], s)) {
41 static bool get_dungeon_id(const char *s, int *id)
43 if (strcmp(s, "dungeon") == 0) {
45 long lv = strtol(s + 7, &pe, 10);
47 if (pe != s && !*pe && lv > INT_MIN && lv < INT_MAX) {
56 static bool get_quest_id(const char *s, int *id)
58 if (strcmp(s, "quest") == 0) {
60 long lv = strtol(s + 5, &pe, 10);
62 if (pe != s && !*pe && lv > INT_MIN && lv < INT_MAX) {
71 static bool get_town_id(const char *s, int *id)
73 if (strcmp(s, "town") == 0) {
75 long lv = strtol(s + 4, &pe, 10);
77 if (pe != s && !*pe && lv > INT_MIN && lv < INT_MAX) {
86 static bool get_monster_id(const char *s, int *id)
88 if (strcmp(s, "monster") == 0) {
90 long lv = strtol(s + 7, &pe, 10);
92 if (pe != s && !*pe && lv > INT_MIN && lv < INT_MAX) {
101 @implementation AngbandActiveAudio
104 * Handle property methods where need more than is provided by the default
109 return self->player && [self->player isPlaying];
113 - (id)initWithPlayer:(AVAudioPlayer *)aPlayer fadeInBy:(NSInteger)fadeIn
114 prior:(AngbandActiveAudio *)p paused:(BOOL)isPaused
116 if (self = [super init]) {
117 self->_priorAudio = p;
119 self->_nextAudio = p->_nextAudio;
121 p->_nextAudio->_priorAudio = self;
123 p->_nextAudio = self;
125 self->_nextAudio = nil;
127 self->player = aPlayer;
128 self->fadeTimer = nil;
130 aPlayer.delegate = self;
132 float volume = [aPlayer volume];
135 [aPlayer setVolume:volume
136 fadeDuration:(fadeIn * .001)];
150 [self->player pause];
166 if (self->fadeTimer) {
167 [self->fadeTimer invalidate];
168 self->fadeTimer = nil;
175 - (void)fadeOutBy:(NSInteger)t
177 /* Only fade out if there's a track and it is not already fading out. */
178 if (self->player && !self->fadeTimer) {
179 [self->player setVolume:0.0f
180 fadeDuration:(((t > 0) ? t : 0) * .001)];
181 /* Set up a timer to remove the faded out track. */
182 self->fadeTimer = [NSTimer
183 scheduledTimerWithTimeInterval:(t * .001 + .01)
185 selector:@selector(handleFadeOutTimer:)
188 self->fadeTimer.tolerance = 0.02;
193 - (void)changeVolumeTo:(NSInteger)v
200 } else if (v > 100) {
205 self->player.volume = safev * .01f;
210 - (void)handleFadeOutTimer:(NSTimer *)timer
212 assert(self->player && self->fadeTimer == timer);
213 self->fadeTimer = nil;
218 - (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)aPlayer
219 successfully:(BOOL)flag
221 assert(aPlayer == self->player);
222 if (self->fadeTimer) {
223 [self->fadeTimer invalidate];
224 self->fadeTimer = nil;
227 /* Unlink from the list of active tracks. */
228 assert([self priorAudio] && [self nextAudio]);
229 self->_priorAudio->_nextAudio = self->_nextAudio;
230 self->_nextAudio->_priorAudio = self->_priorAudio;
231 self->_priorAudio = nil;
232 self->_nextAudio = nil;
238 @implementation AngbandAudioManager
240 @synthesize soundVolume=_soundVolume;
241 @synthesize musicEnabled=_musicEnabled;
242 @synthesize musicVolume=_musicVolume;
245 * Handle property methods where need more than provided by the default
248 - (void)setSoundVolume:(NSInteger)v
250 /* Incidental sounds that are currently playing aren't changed. */
252 self->_soundVolume = 0;
253 } else if (v > 100) {
254 self->_soundVolume = 100;
256 self->_soundVolume = v;
261 - (void)setMusicEnabled:(BOOL)b
263 BOOL old = self->_musicEnabled;
265 self->_musicEnabled = b;
268 AngbandActiveAudio *a = [self->tracksPlayingHead nextAudio];
271 AngbandActiveAudio *t = a;
280 - (void)setMusicVolume:(NSInteger)v
282 NSInteger old = self->_musicVolume;
285 self->_musicVolume = 0;
286 } else if (v > 100) {
287 self->_musicVolume = 100;
289 self->_musicVolume = v;
293 [[self->tracksPlayingTail priorAudio]
294 changeVolumeTo:self->_musicVolume];
299 /* Define methods. */
302 if (self = [super init]) {
303 self->tracksPlayingHead = [[AngbandActiveAudio alloc]
304 initWithPlayer:nil fadeInBy:0 prior:nil paused:NO];
305 self->tracksPlayingTail = [[AngbandActiveAudio alloc]
306 initWithPlayer:nil fadeInBy:0
307 prior:self->tracksPlayingHead paused:NO];
308 self->soundArraysByEvent = nil;
309 self->musicByTypeAndID = nil;
310 self->appActive = YES;
311 self->_beepEnabled = YES;
312 self->_soundEnabled = YES;
313 self->_soundVolume = 30;
314 self->_musicEnabled = YES;
315 self->_musicPausedWhenInactive = YES;
316 self->_musicVolume = 20;
317 self->_musicTransitionTime = 3000;
325 if (!self->appActive || ![self isBeepEnabled]) {
329 * Use NSBeep() for this, though that means it doesn't heed the
330 * volume set by the soundVolume property.
335 - (void)playSound:(int)event
337 if (!self->appActive || ![self isSoundEnabled]) {
341 /* Initialize when the first sound is played. */
342 if (!self->soundArraysByEvent) {
343 self->soundArraysByEvent =
344 [AngbandAudioManager setupSoundArraysByEvent];
345 if (!self->soundArraysByEvent) {
351 NSMutableArray *samples = [self->soundArraysByEvent
352 objectForKey:[NSNumber numberWithInteger:event]];
353 AVAudioPlayer *player;
356 if (!samples || !samples.count) {
360 s = randint0((int)samples.count);
363 if ([player isPlaying]) {
365 player.currentTime = 0;
367 player.volume = self.soundVolume * .01f;
373 - (void)playMusicType:(int)t ID:(int)i
375 if (![self isMusicEnabled]) {
379 /* Initialize when the first music track is played. */
380 if (!self->musicByTypeAndID) {
381 self->musicByTypeAndID =
382 [AngbandAudioManager setupMusicByTypeAndID];
386 NSMutableDictionary *musicByID = [self->musicByTypeAndID
387 objectForKey:[NSNumber numberWithInteger:t]];
388 NSMutableArray *paths;
390 AVAudioPlayer *player;
396 objectForKey:[NSNumber numberWithInteger:i]];
397 if (!paths || !paths.count) {
400 audioData = [NSData dataWithContentsOfFile:paths[randint0((int)paths.count)]];
401 player = [[AVAudioPlayer alloc] initWithData:audioData
405 AngbandActiveAudio *prior_track =
406 [self->tracksPlayingTail priorAudio];
407 AngbandActiveAudio *active_track;
410 player.volume = 0.01f * [self musicVolume];
411 if ([prior_track isPlaying]) {
412 fade_time = [self musicTransitionTime];
416 [prior_track fadeOutBy:fade_time];
420 active_track = [[AngbandActiveAudio alloc]
421 initWithPlayer:player fadeInBy:fade_time
423 paused:(!self->appActive && [self isMusicPausedWhenInactive])];
429 - (BOOL)musicExists:(int)t ID:(int)i
431 NSMutableDictionary *musicByID;
434 /* Initialize on the first call if it hasn't already been done. */
435 if (!self->musicByTypeAndID) {
436 self->musicByTypeAndID =
437 [AngbandAudioManager setupMusicByTypeAndID];
440 musicByID = [self->musicByTypeAndID
441 objectForKey:[NSNumber numberWithInteger:t]];
444 NSString *path = [musicByID
445 objectForKey:[NSNumber numberWithInteger:i]];
447 exists = (path != nil);
458 AngbandActiveAudio *track = [self->tracksPlayingHead nextAudio];
462 track = [track nextAudio];
470 - (void)setupForInactiveApp
472 if (!self->appActive) {
475 self->appActive = NO;
476 if ([self isMusicPausedWhenInactive]) {
477 AngbandActiveAudio *track =
478 [self->tracksPlayingHead nextAudio];
481 AngbandActiveAudio *next_track = [track nextAudio];
483 if (next_track != self->tracksPlayingTail) {
484 /* Stop all tracks but the last one playing. */
488 * Pause the last track playing. Set its
489 * volume to maximum so, when resumed, it'll
490 * play that way even if it was fading in when
494 [track changeVolumeTo:[self musicVolume]];
505 - (void)setupForActiveApp
507 if (self->appActive) {
510 self->appActive = YES;
511 if ([self isMusicPausedWhenInactive]) {
512 /* Resume any tracks that were playing. */
513 AngbandActiveAudio *track =
514 [self->tracksPlayingTail priorAudio];
518 track = [track priorAudio];
527 /* Set up the class properties. */
528 static NSInteger _maxSamples = 16;
529 + (NSInteger)maxSamples
535 static AngbandAudioManager *_sharedManager = nil;
536 + (AngbandAudioManager *)sharedManager
538 if (!_sharedManager) {
539 _sharedManager = [[AngbandAudioManager alloc] init];
541 return _sharedManager;
545 /* Define class methods. */
546 + (void)clearSharedManager
548 _sharedManager = nil;
552 + (NSMutableDictionary *)setupSoundArraysByEvent
554 std::filesystem::path psound, p;
556 NSMutableDictionary *arraysByEvent;
558 /* Build the "sound" path. */
559 psound = path_build(ANGBAND_DIR_XTRA, "sound");
561 /* Find and open the config file. */
562 p = path_build(psound, "sound.cfg");
563 fff = angband_fopen(p, FileOpenMode::READ);
566 NSLog(@"The sound configuration file could not be opened");
570 arraysByEvent = [[NSMutableDictionary alloc] init];
573 * This loop may take a while depending on the count and size
574 * of samples to load.
576 const char white[] = " \t";
577 NSMutableDictionary *playersByPath =
578 [[NSMutableDictionary alloc] init];
581 /* Parse the file. */
582 /* Lines are always of the form "name = sample [sample ...]". */
583 while (angband_fgets(fff, buffer, sizeof(buffer)) == 0) {
584 NSMutableArray *soundSamples;
591 /* Skip leading whitespace. */
592 skip = strspn(buffer, white);
595 * Ignore anything not beginning with an alphabetic
598 if (!buffer[skip] || !isalpha((unsigned char)buffer[skip])) {
603 * Split the line into two; message name and the rest.
605 search = strchr(buffer + skip, '=');
609 msg_name = buffer + skip;
610 skip = strcspn(msg_name, white);
611 if (skip > (size_t)(search - msg_name)) {
613 * No white space between the message name and
618 msg_name[skip] = '\0';
620 skip = strspn(search + 1, white);
621 sample_name = search + 1 + skip;
623 /* Make sure this is a valid event name. */
624 for (match = SOUND_MAX - 1; match >= 0; --match) {
625 if (!strcmp(msg_name, angband_sound_name[match])) {
633 soundSamples = [arraysByEvent
634 objectForKey:[NSNumber numberWithInteger:match]];
636 soundSamples = [[NSMutableArray alloc] init];
638 setObject:soundSamples
639 forKey:[NSNumber numberWithInteger:match]];
643 * Now find all the sample names and add them one by
648 NSString *token_string;
649 AVAudioPlayer *player;
652 if (!sample_name[0]) {
655 /* Terminate the current token. */
656 skip = strcspn(sample_name, white);
657 done = !sample_name[skip];
658 sample_name[skip] = '\0';
660 /* Don't allow too many samples. */
661 num = (int) soundSamples.count;
662 if (num >= [AngbandAudioManager maxSamples]) {
666 token_string = [NSString
667 stringWithUTF8String:sample_name];
668 player = [playersByPath
669 objectForKey:token_string];
672 * We have to load the sound.
673 * Build the path to the sample.
677 p = path_build(psound, sample_name);
678 if (stat(p.native().data(), &stb) == 0
679 && (stb.st_mode & S_IFREG)) {
680 NSData *audioData = [NSData
681 dataWithContentsOfFile:[NSString stringWithUTF8String:p.native().data()]];
683 player = [[AVAudioPlayer alloc]
684 initWithData:audioData
689 forKey:token_string];
694 /* Store it if it was loaded. */
696 [soundSamples addObject:player];
702 sample_name += skip + 1;
703 skip = strspn(sample_name, white);
711 return arraysByEvent;
715 + (NSMutableDictionary *)setupMusicByTypeAndID
721 bool (*name2id_func)(const char*, int*);
722 } sections_of_interest[] = {
723 { "Basic", TERM_XTRA_MUSIC_BASIC, NULL, get_basic_id },
724 { "Dungeon", TERM_XTRA_MUSIC_DUNGEON, NULL, get_dungeon_id },
725 { "Quest", TERM_XTRA_MUSIC_QUEST, NULL, get_quest_id },
726 { "Town", TERM_XTRA_MUSIC_TOWN, NULL, get_town_id },
727 { "Monster", TERM_XTRA_MUSIC_MONSTER, &has_monster_music,
729 { NULL, 0, NULL, NULL } /* terminating sentinel */
731 std::filesystem::path pmusic, p;
733 NSMutableDictionary *catalog;
735 /* Build the "music" path. */
736 pmusic = path_build(ANGBAND_DIR_XTRA, "music");
738 /* Find and open the config file. */
739 p = path_build(pmusic, "music.cfg");
740 fff = angband_fopen(p, FileOpenMode::READ);
743 NSLog(@"The music configuration file could not be opened");
747 catalog = [[NSMutableDictionary alloc] init];
749 const char white[] = " \t";
750 NSMutableDictionary *catalogTypeRestricted = nil;
754 /* Parse the file. */
755 while (angband_fgets(fff, buffer, sizeof(buffer)) == 0) {
756 NSMutableArray *samples;
763 /* Skip leading whitespace. */
764 skip = strspn(buffer, white);
767 * Ignore empty lines or ones that only have comments.
769 if (!buffer[skip] || buffer[skip] == '#') {
773 if (buffer[skip] == '[') {
775 * Found the start of a new section. Will do
776 * nothing if the section name is malformed
777 * (i.e. missing the trailing bracket).
779 search = strchr(buffer + skip + 1, ']');
785 if (!sections_of_interest[isec].name) {
786 catalogTypeRestricted =
791 if (!strcmp(sections_of_interest[isec].name, buffer + skip + 1)) {
792 NSNumber *key = [NSNumber
793 numberWithInteger:sections_of_interest[isec].type_code];
794 catalogTypeRestricted =
795 [catalog objectForKey:key];
796 if (!catalogTypeRestricted) {
797 catalogTypeRestricted =
798 [[NSMutableDictionary alloc] init];
800 setObject:catalogTypeRestricted
808 /* Skip the rest of the line. */
813 * Targets should begin with an alphabetical character.
814 * Skip anything else.
816 if (!isalpha((unsigned char)buffer[skip])) {
820 search = strchr(buffer + skip, '=');
824 id_name = buffer + skip;
825 skip = strcspn(id_name, white);
826 if (skip > (size_t)(search - id_name)) {
828 * No white space between the name on the left
833 id_name[skip] = '\0';
835 skip = strspn(search + 1, white);
836 sample_name = search + 1 + skip;
838 if (!catalogTypeRestricted
839 || (*(sections_of_interest[isec].name2id_func))(id_name, &id)) {
841 * It is not in a section of interest or did
842 * not recognize what was on the left side of
843 * '='. Ignore the line.
847 if (sections_of_interest[isec].p_has) {
848 *(sections_of_interest[isec].p_has) = true;
850 samples = [catalogTypeRestricted
851 objectForKey:[NSNumber numberWithInteger:id]];
853 samples = [[NSMutableArray alloc] init];
854 [catalogTypeRestricted
856 forKey:[NSNumber numberWithInteger:id]];
860 * Now find all the sample names and add them one by
867 if (!sample_name[0]) {
870 /* Terminate the current token. */
871 skip = strcspn(sample_name, white);
872 done = !sample_name[skip];
873 sample_name[skip] = '\0';
876 * Check if the path actually corresponds to a
877 * file. Also restrict the number of samples
878 * stored for any type/ID combination.
880 p = path_build(pmusic, sample_name);
881 if (stat(p.native().data(), &stb) == 0
882 && (stb.st_mode & S_IFREG)
883 && (int)samples.count
884 < [AngbandAudioManager maxSamples]) {
885 [samples addObject:[NSString
886 stringWithUTF8String:p.native().data()]];
892 sample_name += skip + 1;
893 skip = strspn(sample_name, white);