OSDN Git Service

Merge branch 'develop' into macos-develop
[hengbandforosx/hengbandosx.git] / src / cocoa / AngbandAudio.mm
1 /**
2  * \file AngbandAudio.mm
3  * \brief Define an interface for handling incidental sounds and background
4  * music in the OS X front end.
5  */
6
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"
14 #include <limits.h>
15 #include <string.h>
16 #include <ctype.h>
17
18
19 /*
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.
23  */
24 static bool get_basic_id(const char *s, int *id)
25 {
26         int i = 0;
27
28         while (1) {
29                 if (i >= MUSIC_BASIC_MAX) {
30                         return true;
31                 }
32                 if (!strcmp(angband_music_basic_name[i], s)) {
33                         *id = i;
34                         return false;
35                 }
36                 ++i;
37         }
38 }
39
40
41 static bool get_dungeon_id(const char *s, int *id)
42 {
43         if (strcmp(s, "dungeon") == 0) {
44                 char *pe;
45                 long lv = strtol(s + 7, &pe, 10);
46
47                 if (pe != s && !*pe && lv > INT_MIN && lv < INT_MAX) {
48                         *id = (int)lv;
49                         return false;
50                 }
51         }
52         return true;
53 }
54
55
56 static bool get_quest_id(const char *s, int *id)
57 {
58         if (strcmp(s, "quest") == 0) {
59                 char *pe;
60                 long lv = strtol(s + 5, &pe, 10);
61
62                 if (pe != s && !*pe && lv > INT_MIN && lv < INT_MAX) {
63                         *id = (int)lv;
64                         return false;
65                 }
66         }
67         return true;
68 }
69
70
71 static bool get_town_id(const char *s, int *id)
72 {
73         if (strcmp(s, "town") == 0) {
74                 char *pe;
75                 long lv = strtol(s + 4, &pe, 10);
76
77                 if (pe != s && !*pe && lv > INT_MIN && lv < INT_MAX) {
78                         *id = (int)lv;
79                         return false;
80                 }
81         }
82         return true;
83 }
84
85
86 static bool get_monster_id(const char *s, int *id)
87 {
88         if (strcmp(s, "monster") == 0) {
89                 char *pe;
90                 long lv = strtol(s + 7, &pe, 10);
91
92                 if (pe != s && !*pe && lv > INT_MIN && lv < INT_MAX) {
93                         *id = (int)lv;
94                         return false;
95                 }
96         }
97         return true;
98 }
99
100
101 @implementation AngbandActiveAudio
102
103 /*
104  * Handle property methods where need more than is provided by the default
105  * synthesis.
106  */
107 - (BOOL) isPlaying
108 {
109         return self->player && [self->player isPlaying];
110 }
111
112
113 - (id)initWithPlayer:(AVAudioPlayer *)aPlayer fadeInBy:(NSInteger)fadeIn
114                 prior:(AngbandActiveAudio *)p paused:(BOOL)isPaused
115 {
116         if (self = [super init]) {
117                 self->_priorAudio = p;
118                 if (p) {
119                         self->_nextAudio = p->_nextAudio;
120                         if (p->_nextAudio) {
121                                 p->_nextAudio->_priorAudio = self;
122                         }
123                         p->_nextAudio = self;
124                 } else {
125                         self->_nextAudio = nil;
126                 }
127                 self->player = aPlayer;
128                 self->fadeTimer = nil;
129                 if (aPlayer) {
130                         aPlayer.delegate = self;
131                         if (fadeIn > 0) {
132                                 float volume = [aPlayer volume];
133
134                                 aPlayer.volume = 0;
135                                 [aPlayer setVolume:volume
136                                         fadeDuration:(fadeIn * .001)];
137                         }
138                         if (!isPaused) {
139                                 [aPlayer play];
140                         }
141                 }
142         }
143         return self;
144 }
145
146
147 - (void)pause
148 {
149         if (self->player) {
150                 [self->player pause];
151         }
152 }
153
154
155 - (void)resume
156 {
157         if (self->player) {
158                 [self->player play];
159         }
160 }
161
162
163 - (void)stop
164 {
165         if (self->player) {
166                 if (self->fadeTimer) {
167                         [self->fadeTimer invalidate];
168                         self->fadeTimer = nil;
169                 }
170                 [self->player stop];
171         }
172 }
173
174
175 - (void)fadeOutBy:(NSInteger)t
176 {
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)
184                         target:self
185                         selector:@selector(handleFadeOutTimer:)
186                         userInfo:nil
187                         repeats:NO];
188                 self->fadeTimer.tolerance = 0.02;
189         }
190 }
191
192
193 - (void)changeVolumeTo:(NSInteger)v
194 {
195         if (self->player) {
196                 NSInteger safev;
197
198                 if (v < 0) {
199                         safev = 0;
200                 } else if (v > 100) {
201                         safev = 100;
202                 } else {
203                         safev = v;
204                 }
205                 self->player.volume = safev * .01f;
206         }
207 }
208
209
210 - (void)handleFadeOutTimer:(NSTimer *)timer
211 {
212         assert(self->player && self->fadeTimer == timer);
213         self->fadeTimer = nil;
214         [self->player stop];
215 }
216
217
218 - (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)aPlayer
219                 successfully:(BOOL)flag
220 {
221         assert(aPlayer == self->player);
222         if (self->fadeTimer) {
223                 [self->fadeTimer invalidate];
224                 self->fadeTimer = nil;
225         }
226         self->player = 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;
233 }
234
235 @end
236
237
238 @implementation AngbandAudioManager
239
240 @synthesize soundVolume=_soundVolume;
241 @synthesize musicEnabled=_musicEnabled;
242 @synthesize musicVolume=_musicVolume;
243
244 /*
245  * Handle property methods where need more than provided by the default
246  * synthesis.
247  */
248 - (void)setSoundVolume:(NSInteger)v
249 {
250         /* Incidental sounds that are currently playing aren't changed. */
251         if (v < 0) {
252                 self->_soundVolume = 0;
253         } else if (v > 100) {
254                 self->_soundVolume = 100;
255         } else {
256                 self->_soundVolume = v;
257         }
258 }
259
260
261 - (void)setMusicEnabled:(BOOL)b
262 {
263         BOOL old = self->_musicEnabled;
264
265         self->_musicEnabled = b;
266
267         if (!b && old) {
268                 AngbandActiveAudio *a = [self->tracksPlayingHead nextAudio];
269
270                 while (a) {
271                         AngbandActiveAudio *t = a;
272
273                         a = [a nextAudio];
274                         [t stop];
275                 };
276         }
277 }
278
279
280 - (void)setMusicVolume:(NSInteger)v
281 {
282         NSInteger old = self->_musicVolume;
283
284         if (v < 0) {
285                 self->_musicVolume = 0;
286         } else if (v > 100) {
287                 self->_musicVolume = 100;
288         } else {
289                 self->_musicVolume = v;
290         }
291
292         if (v != old) {
293                 [[self->tracksPlayingTail priorAudio]
294                         changeVolumeTo:self->_musicVolume];
295         }
296 }
297
298
299 /* Define methods. */
300 - (id)init
301 {
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;
318         }
319         return self;
320 }
321
322
323 - (void)playBeep
324 {
325         if (!self->appActive || ![self isBeepEnabled]) {
326                 return;
327         }
328         /*
329          * Use NSBeep() for this, though that means it doesn't heed the
330          * volume set by the soundVolume property.
331          */
332         NSBeep();
333 }
334
335 - (void)playSound:(int)event
336 {
337         if (!self->appActive || ![self isSoundEnabled]) {
338                 return;
339         }
340
341         /* Initialize when the first sound is played. */
342         if (!self->soundArraysByEvent) {
343                 self->soundArraysByEvent =
344                         [AngbandAudioManager setupSoundArraysByEvent];
345                 if (!self->soundArraysByEvent) {
346                         return;
347                 }
348         }
349
350         @autoreleasepool {
351                 NSMutableArray *samples = [self->soundArraysByEvent
352                         objectForKey:[NSNumber numberWithInteger:event]];
353                 AVAudioPlayer *player;
354                 int s;
355
356                 if (!samples || !samples.count) {
357                         return;
358                 }
359
360                 s = randint0((int)samples.count);
361                 player = samples[s];
362
363                 if ([player isPlaying]) {
364                         [player stop];
365                         player.currentTime = 0;
366                 }
367                 player.volume = self.soundVolume * .01f;
368                 [player play];
369         }
370 }
371
372
373 - (void)playMusicType:(int)t ID:(int)i
374 {
375         if (![self isMusicEnabled]) {
376                 return;
377         }
378
379         /* Initialize when the first music track is played. */
380         if (!self->musicByTypeAndID) {
381                 self->musicByTypeAndID =
382                         [AngbandAudioManager setupMusicByTypeAndID];
383         }
384
385         @autoreleasepool {
386                 NSMutableDictionary *musicByID = [self->musicByTypeAndID
387                         objectForKey:[NSNumber numberWithInteger:t]];
388                 NSMutableArray *paths;
389                 NSData *audioData;
390                 AVAudioPlayer *player;
391
392                 if (!musicByID) {
393                         return;
394                 }
395                 paths = [musicByID
396                         objectForKey:[NSNumber numberWithInteger:i]];
397                 if (!paths || !paths.count) {
398                         return;
399                 }
400                 audioData = [NSData dataWithContentsOfFile:paths[randint0((int)paths.count)]];
401                 player = [[AVAudioPlayer alloc] initWithData:audioData
402                         error:nil];
403
404                 if (player) {
405                         AngbandActiveAudio *prior_track =
406                                 [self->tracksPlayingTail priorAudio];
407                         AngbandActiveAudio *active_track;
408                         NSInteger fade_time;
409
410                         player.volume = 0.01f * [self musicVolume];
411                         if ([prior_track isPlaying]) {
412                                 fade_time = [self musicTransitionTime];
413                                 if (fade_time < 0) {
414                                         fade_time = 0;
415                                 }
416                                 [prior_track fadeOutBy:fade_time];
417                         } else {
418                                 fade_time = 0;
419                         }
420                         active_track = [[AngbandActiveAudio alloc]
421                                 initWithPlayer:player fadeInBy:fade_time
422                                 prior:prior_track
423                                 paused:(!self->appActive && [self isMusicPausedWhenInactive])];
424                 }
425         }
426 }
427
428
429 - (BOOL)musicExists:(int)t ID:(int)i
430 {
431         NSMutableDictionary *musicByID;
432         BOOL exists;
433
434         /* Initialize on the first call if it hasn't already been done. */
435         if (!self->musicByTypeAndID) {
436                 self->musicByTypeAndID =
437                         [AngbandAudioManager setupMusicByTypeAndID];
438         }
439
440         musicByID = [self->musicByTypeAndID
441                 objectForKey:[NSNumber numberWithInteger:t]];
442
443         if (musicByID) {
444                 NSString *path = [musicByID
445                         objectForKey:[NSNumber numberWithInteger:i]];
446
447                 exists = (path != nil);
448         } else {
449                 exists = NO;
450         }
451
452         return exists;
453 }
454
455
456 - (void)stopAllMusic
457 {
458         AngbandActiveAudio *track = [self->tracksPlayingHead nextAudio];
459
460         while (1) {
461                 [track stop];
462                 track = [track nextAudio];
463                 if (!track) {
464                         break;
465                 }
466         }
467 }
468
469
470 - (void)setupForInactiveApp
471 {
472         if (!self->appActive) {
473                 return;
474         }
475         self->appActive = NO;
476         if ([self isMusicPausedWhenInactive]) {
477                 AngbandActiveAudio *track =
478                         [self->tracksPlayingHead nextAudio];
479
480                 while (1) {
481                         AngbandActiveAudio *next_track = [track nextAudio];
482
483                         if (next_track != self->tracksPlayingTail) {
484                                 /* Stop all tracks but the last one playing. */
485                                 [track stop];
486                         } else {
487                                 /*
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
491                                  * paused.
492                                  */
493                                 [track pause];
494                                 [track changeVolumeTo:[self musicVolume]];
495                         }
496                         track = next_track;
497                         if (!track) {
498                                 break;
499                         }
500                 }
501         }
502 }
503
504
505 - (void)setupForActiveApp
506 {
507         if (self->appActive) {
508                 return;
509         }
510         self->appActive = YES;
511         if ([self isMusicPausedWhenInactive]) {
512                 /* Resume any tracks that were playing. */
513                 AngbandActiveAudio *track =
514                         [self->tracksPlayingTail priorAudio];
515
516                 while (1) {
517                         [track resume];
518                         track = [track priorAudio];
519                         if (!track) {
520                                 break;
521                         }
522                 }
523         }
524 }
525
526
527 /* Set up the class properties. */
528 static NSInteger _maxSamples = 16;
529 + (NSInteger)maxSamples
530 {
531         return _maxSamples;
532 }
533
534
535 static AngbandAudioManager *_sharedManager = nil;
536 + (AngbandAudioManager *)sharedManager
537 {
538         if (!_sharedManager) {
539                 _sharedManager = [[AngbandAudioManager alloc] init];
540         }
541         return _sharedManager;
542 }
543
544
545 /* Define class methods. */
546 + (void)clearSharedManager
547 {
548         _sharedManager = nil;
549 }
550
551
552 + (NSMutableDictionary *)setupSoundArraysByEvent
553 {
554         std::filesystem::path psound, p;
555         FILE *fff;
556         NSMutableDictionary *arraysByEvent;
557
558         /* Build the "sound" path. */
559         psound = path_build(ANGBAND_DIR_XTRA, "sound");
560
561         /* Find and open the config file. */
562         p = path_build(psound, "sound.cfg");
563         fff = angband_fopen(p, FileOpenMode::READ);
564
565         if (!fff) {
566                 NSLog(@"The sound configuration file could not be opened");
567                 return nil;
568         }
569
570         arraysByEvent = [[NSMutableDictionary alloc] init];
571         @autoreleasepool {
572                 /*
573                  * This loop may take a while depending on the count and size
574                  * of samples to load.
575                  */
576                 const char white[] = " \t";
577                 NSMutableDictionary *playersByPath =
578                         [[NSMutableDictionary alloc] init];
579                 char buffer[2048];
580
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;
585                         char *msg_name;
586                         char *sample_name;
587                         char *search;
588                         int match;
589                         size_t skip;
590
591                         /* Skip leading whitespace. */
592                         skip = strspn(buffer, white);
593
594                         /*
595                          * Ignore anything not beginning with an alphabetic
596                          * character.
597                          */
598                         if (!buffer[skip] || !isalpha((unsigned char)buffer[skip])) {
599                                 continue;
600                         }
601
602                         /*
603                          * Split the line into two; message name and the rest.
604                          */
605                         search = strchr(buffer + skip, '=');
606                         if (!search) {
607                                 continue;
608                         }
609                         msg_name = buffer + skip;
610                         skip = strcspn(msg_name, white);
611                         if (skip > (size_t)(search - msg_name)) {
612                                 /*
613                                  *  No white space between the message name and
614                                  * '='.
615                                  */
616                                 *search = '\0';
617                         } else {
618                                 msg_name[skip] = '\0';
619                         }
620                         skip = strspn(search + 1, white);
621                         sample_name = search + 1 + skip;
622
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])) {
626                                         break;
627                                 }
628                         }
629                         if (match < 0) {
630                                 continue;
631                         }
632
633                         soundSamples = [arraysByEvent
634                                 objectForKey:[NSNumber numberWithInteger:match]];
635                         if (!soundSamples) {
636                                 soundSamples = [[NSMutableArray alloc] init];
637                                 [arraysByEvent
638                                         setObject:soundSamples
639                                         forKey:[NSNumber numberWithInteger:match]];
640                         }
641
642                         /*
643                          * Now find all the sample names and add them one by
644                          * one.
645                          */
646                         while (1) {
647                                 int num;
648                                 NSString *token_string;
649                                 AVAudioPlayer *player;
650                                 BOOL done;
651
652                                 if (!sample_name[0]) {
653                                         break;
654                                 }
655                                 /* Terminate the current token. */
656                                 skip = strcspn(sample_name, white);
657                                 done = !sample_name[skip];
658                                 sample_name[skip] = '\0';
659
660                                 /* Don't allow too many samples. */
661                                 num = (int) soundSamples.count;
662                                 if (num >= [AngbandAudioManager maxSamples]) {
663                                         break;
664                                 }
665
666                                 token_string = [NSString
667                                         stringWithUTF8String:sample_name];
668                                 player = [playersByPath
669                                         objectForKey:token_string];
670                                 if (!player) {
671                                         /*
672                                          * We have to load the sound.
673                                          * Build the path to the sample.
674                                          */
675                                         struct stat stb;
676
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()]];
682
683                                                 player = [[AVAudioPlayer alloc]
684                                                         initWithData:audioData
685                                                         error:nil];
686                                                 if (player) {
687                                                         [playersByPath
688                                                                 setObject:player
689                                                                 forKey:token_string];
690                                                 }
691                                         }
692                                 }
693
694                                 /* Store it if it was loaded. */
695                                 if (player) {
696                                         [soundSamples addObject:player];
697                                 }
698
699                                 if (done) {
700                                         break;
701                                 }
702                                 sample_name += skip + 1;
703                                 skip = strspn(sample_name, white);
704                                 sample_name += skip;
705                         }
706                 }
707                 playersByPath = nil;
708         }
709         angband_fclose(fff);
710
711         return arraysByEvent;
712 }
713
714
715 + (NSMutableDictionary *)setupMusicByTypeAndID
716 {
717         struct {
718                 const char * name;
719                 int type_code;
720                 bool *p_has;
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,
728                         get_monster_id },
729                 { NULL, 0, NULL, NULL } /* terminating sentinel */
730         };
731         std::filesystem::path pmusic, p;
732         FILE *fff;
733         NSMutableDictionary *catalog;
734
735         /* Build the "music" path. */
736         pmusic = path_build(ANGBAND_DIR_XTRA, "music");
737
738         /* Find and open the config file. */
739         p = path_build(pmusic, "music.cfg");
740         fff = angband_fopen(p, FileOpenMode::READ);
741
742         if (!fff) {
743                 NSLog(@"The music configuration file could not be opened");
744                 return nil;
745         }
746
747         catalog = [[NSMutableDictionary alloc] init];
748         @autoreleasepool {
749                 const char white[] = " \t";
750                 NSMutableDictionary *catalogTypeRestricted = nil;
751                 int isec = -1;
752                 char buffer[2048];
753
754                 /* Parse the file. */
755                 while (angband_fgets(fff, buffer, sizeof(buffer)) == 0) {
756                         NSMutableArray *samples;
757                         char *id_name;
758                         char *sample_name;
759                         char *search;
760                         size_t skip;
761                         int id;
762
763                         /* Skip leading whitespace. */
764                         skip = strspn(buffer, white);
765
766                         /*
767                          * Ignore empty lines or ones that only have comments.
768                          */
769                         if (!buffer[skip] || buffer[skip] == '#') {
770                                 continue;
771                         }
772
773                         if (buffer[skip] == '[') {
774                                 /*
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).
778                                  */
779                                 search = strchr(buffer + skip + 1, ']');
780                                 if (search) {
781                                         isec = 0;
782
783                                         *search = '\0';
784                                         while (1) {
785                                                 if (!sections_of_interest[isec].name) {
786                                                         catalogTypeRestricted =
787                                                                 nil;
788                                                         isec = -1;
789                                                         break;
790                                                 }
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];
799                                                                 [catalog
800                                                                         setObject:catalogTypeRestricted
801                                                                         forKey:key];
802                                                         }
803                                                         break;
804                                                 }
805                                                 ++isec;
806                                         }
807                                 }
808                                 /* Skip the rest of the line. */
809                                 continue;
810                         }
811
812                         /*
813                          * Targets should begin with an alphabetical character.
814                          * Skip anything else.
815                          */
816                         if (!isalpha((unsigned char)buffer[skip])) {
817                                 continue;
818                         }
819
820                         search = strchr(buffer + skip, '=');
821                         if (!search) {
822                                 continue;
823                         }
824                         id_name = buffer + skip;
825                         skip = strcspn(id_name, white);
826                         if (skip > (size_t)(search - id_name)) {
827                                 /*
828                                  * No white space between the name on the left
829                                  * and '='.
830                                  */
831                                 *search = '\0';
832                         } else {
833                                 id_name[skip] = '\0';
834                         }
835                         skip = strspn(search + 1, white);
836                         sample_name = search + 1 + skip;
837
838                         if (!catalogTypeRestricted
839                                         || (*(sections_of_interest[isec].name2id_func))(id_name, &id)) {
840                                 /*
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.
844                                  */
845                                 continue;
846                         }
847                         if (sections_of_interest[isec].p_has) {
848                                 *(sections_of_interest[isec].p_has) = true;
849                         }
850                         samples = [catalogTypeRestricted
851                                 objectForKey:[NSNumber numberWithInteger:id]];
852                         if (!samples) {
853                                 samples = [[NSMutableArray alloc] init];
854                                 [catalogTypeRestricted
855                                         setObject:samples
856                                         forKey:[NSNumber numberWithInteger:id]];
857                         }
858
859                         /*
860                          * Now find all the sample names and add them one by
861                          * one.
862                          */
863                         while (1) {
864                                 BOOL done;
865                                 struct stat stb;
866
867                                 if (!sample_name[0]) {
868                                         break;
869                                 }
870                                 /* Terminate the current token. */
871                                 skip = strcspn(sample_name, white);
872                                 done = !sample_name[skip];
873                                 sample_name[skip] = '\0';
874
875                                 /*
876                                  * Check if the path actually corresponds to a
877                                  * file.  Also restrict the number of samples
878                                  * stored for any type/ID combination.
879                                  */
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()]];
887                                 }
888
889                                 if (done) {
890                                         break;
891                                 }
892                                 sample_name += skip + 1;
893                                 skip = strspn(sample_name, white);
894                                 sample_name += skip;
895                         }
896                 }
897         }
898         angband_fclose(fff);
899
900         return catalog;
901 }
902
903 @end