Resolves https://github.com/backwardsEric/hengband/issues/28 .
cocoa_xcode_files = \
cocoa/AppDelegate.m \
- cocoa/Base.lproj/MainMenu.xib
+ cocoa/Base.lproj/MainMenu.xib \
+ cocoa/Base.lproj/SoundAndMusic.xib
cocoa_icon_files = \
cocoa/hengband_Icons.icns \
cocoa/Save.icns \
cocoa_plist_files = \
cocoa/CommandMenu.plist
cocoa_en_nib_files = \
- cocoa/Base.lproj/MainMenu.nib
+ cocoa/Base.lproj/MainMenu.nib \
+ cocoa/Base.lproj/SoundAndMusic.nib
cocoa_en_strings_files = \
cocoa/en.lproj/Localizable.strings \
cocoa/en.lproj/CommandMenu.strings \
cocoa/en.lproj/GraphicsMenu.strings
cocoa_ja_strings_files = \
cocoa/ja.lproj/MainMenu.strings \
+ cocoa/ja.lproj/SoundAndMusic.strings \
cocoa/ja.lproj/Localizable.strings \
cocoa/ja.lproj/CommandMenu.strings \
cocoa/ja.lproj/GraphicsMenu.strings
main-cocoa.mm \
system/grafmode.h \
system/grafmode.cpp \
- cocoa/AppDelegate.h
+ cocoa/AppDelegate.h \
+ cocoa/AngbandAudio.h \
+ cocoa/AngbandAudio.mm \
+ cocoa/SoundAndMusic.h \
+ cocoa/SoundAndMusic.mm
AM_CFLAGS = -mmacosx-version-min=10.13 -Wunguarded-availability
AM_OBJCXXFLAGS = -std=c++20 -fobjc-arc -mmacosx-version-min=10.13 -Wunguarded-availability -stdlib=libc++
AM_CXXFLAGS = -mmacosx-version-min=10.13 -Wunguarded-availability -stdlib=libc++
-hengband_LDFLAGS = -framework cocoa $(AM_LDFLAGS)
+hengband_LDFLAGS = -framework cocoa -framework AVFoundation $(AM_LDFLAGS)
hengband_LINK = MACOSX_DEPLOYMENT_TARGET=10.13 $(OBJCXXLINK) $(hengband_LDFLAGS) $(LDFLAGS) -o $@
APPNAME = $(PACKAGE_NAME)
APPEXE = hengband
hengband_LINK = $(CXXLINK)
endif
-DEFAULT_INCLUDES = -I$(srcdir) -I$(top_builddir)/src
+# The "-I$(top_builddir)/src/cocoa" is there so can use the same include
+# directives in the cocoa/*.{h,mm} files when building here or rebuilding the
+# nib files in Xcode according to the procedure in cocoa/AppDelegate.m.
+DEFAULT_INCLUDES = -I$(srcdir) -I$(top_builddir)/src -I$(top_builddir)/src/cocoa
CPPFLAGS += $(XFT_CFLAGS) $(libcurl_CFLAGS)
LIBS += $(XFT_LIBS) $(libcurl_LIBS)
COMPILE = $(srcdir)/gcc-wrap $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) \
--- /dev/null
+/**
+ * \file AngbandAudio.h
+ * \brief Declare an interface for handling incidental sounds and background
+ * music in the OS X front end.
+ */
+#ifndef INCLUDED_ANGBAND_AUDIO_H
+#define INCLUDED_ANGBAND_AUDIO_H
+
+#import <AVFAudio/AVFAudio.h>
+
+@class AngbandAudioManager;
+
+
+@interface AngbandActiveAudio : NSObject <AVAudioPlayerDelegate> {
+@private
+ AVAudioPlayer *player;
+ NSTimer *fadeTimer;
+}
+
+@property (nonatomic, readonly, getter=isPlaying) BOOL playing;
+
+@property (nonatomic, readonly, weak) AngbandActiveAudio *priorAudio;
+
+@property (nonatomic, readonly, strong) AngbandActiveAudio *nextAudio;
+
+- (id)initWithPlayer:(AVAudioPlayer *)aPlayer fadeInBy:(NSInteger)fadeIn
+ prior:(AngbandActiveAudio *)p paused:(BOOL)isPaused
+ NS_DESIGNATED_INITIALIZER;
+
+- (id)init NS_UNAVAILABLE;
+
+- (void)pause;
+
+- (void)resume;
+
+- (void)stop;
+
+- (void)fadeOutBy:(NSInteger)t;
+
+- (void)changeVolumeTo:(NSInteger)v;
+
+/* Methods for internal use to manage the lifetime of the active audio track. */
+- (void)handleFadeOutTimer:(NSTimer *)timer;
+
+- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)aPlayer
+ successfully:(BOOL)flag;
+
+@end
+
+
+@interface AngbandAudioManager : NSObject {
+@private
+ /**
+ * These are sentinels for the beginning and end, respectively, of
+ * the background muisc tracks currently playing in the order they
+ * were started.
+ */
+ AngbandActiveAudio *tracksPlayingHead;
+ AngbandActiveAudio *tracksPlayingTail;
+
+ /**
+ * Stores instances of AVAudioPlayer keyed by path so the same
+ * incidental sound can be used for multiple events.
+ */
+ NSMutableDictionary *soundsByPath;
+
+ /**
+ * Stores arrays of AVAudioPlayer instances keyed by event number.
+ */
+ NSMutableDictionary *soundArraysByEvent;
+
+ /**
+ * Stores paths to music tracks keyed first by the music type and
+ * then the music id.
+ */
+ NSMutableDictionary *musicByTypeAndID;
+
+ /**
+ * If NO, then do not play beeps or incidental sounds and pause
+ * the background music track if isPausedWhenInactive is YES.
+ */
+ BOOL appActive;
+}
+
+/**
+ * Holds whether a beep will be played. If NO, then playBeep effectively
+ * becomes a do nothing operation.
+ */
+@property (nonatomic, getter=isBeepEnabled) BOOL beepEnabled;
+
+/**
+ * Holds whether incidental sounds will be played. If NO, then playSound
+ * effectively becomes a do nothing operation.
+ */
+@property (nonatomic, getter=isSoundEnabled) BOOL soundEnabled;
+
+/**
+ * Holds the volume (as a percentage, 0 - 100, of the system volume) for
+ * incidental sounds.
+ */
+@property (nonatomic) NSInteger soundVolume;
+
+/**
+ * Holds whether background music will be played. If NO, then playMusicType
+ * effectively becomes a do nothing operation.
+ */
+@property (nonatomic, getter=isMusicEnabled) BOOL musicEnabled;
+
+/**
+ * Holds whether music is paused when the application is iconified or otherwise
+ * marked as being an inactive application. Beeps and incidental sounds are
+ * never played when the application is an inactive application.
+ */
+@property (nonatomic, getter=isMusicPausedWhenInactive)
+ BOOL musicPausedWhenInactive;
+
+/**
+ * Holds the volume (as a percentage, 0 - 100, of the system volume) for
+ * background music.
+ */
+@property (nonatomic) NSInteger musicVolume;
+
+/**
+ * Holds the amount of time, in milliseconds, for transitions between
+ * different music tracks. If less than or equal to zero, there's no
+ * transition interval: one track stops playing and the other starts playing
+ * at full volume.
+ */
+@property (nonatomic) NSInteger musicTransitionTime;
+
+/**
+ * Set up for lazy initialization in playSound() and playMusicType(). Set
+ * appActive to YES, beepEnabled to YES, soundEnabled to YES, soundVolume
+ * to 30, musicEnabled to YES, pausedWhenInactive to YES,
+ * musicVolume to 20, and musicTransitionTime to 3000.
+ */
+- (id)init;
+
+/**
+ * If self.beepEnabed is YES, make a beep.
+ */
+- (void)playBeep;
+
+/**
+ * If self.soundEnabled is YES and the given event has one or more sounds
+ * corresponding to it in the catalog, plays one of those sounds, chosen at
+ * random.
+ */
+- (void)playSound:(int)event;
+
+/**
+ * If self.musicEnabled is YES and the given music type and id exists, play
+ * it.
+ */
+- (void)playMusicType:(int)t ID:(int)i;
+
+/**
+ * Return YES if combination of the given type and ID is in the music
+ * catalog. Otherwise, return NO.
+ */
+- (BOOL)musicExists:(int)t ID:(int)i;
+
+/**
+ * Stop all currently playing music tracks.
+ */
+- (void)stopAllMusic;
+
+/**
+ * Set up to act appropiately if the containing application is inactive.
+ */
+- (void)setupForInactiveApp;
+
+/**
+ * Set up to act appropriately if the containing application is active.
+ */
+- (void)setupForActiveApp;
+
+/**
+ * Impose an arbitrary limit on the number of possible samples for an
+ * incidental sound event or the background music tracks for a type/ID
+ * combination.
+ */
+@property (class, nonatomic, readonly) NSInteger maxSamples;
+
+/**
+ * Return the shared audio manager instance, creating it if it does not
+ * exist yet.
+ */
+@property (class, nonatomic, strong, readonly) AngbandAudioManager
+ *sharedManager;
+
+/**
+ * Release any resources associated with the shared audio manager.
+ */
++ (void)clearSharedManager;
+
+/**
+ * Help playSound() to set up a catalog of the incidental sounds.
+ */
++ (NSMutableDictionary *)setupSoundArraysByEvent;
+
+/**
+ * Help playMusicType() to set up a catalog of the background music.
+ */
++ (NSMutableDictionary *)setupMusicByTypeAndID;
+
+@end
+
+
+#endif /* include guard */
--- /dev/null
+/**
+ * \file AngbandAudio.mm
+ * \brief Define an interface for handling incidental sounds and background
+ * music in the OS X front end.
+ */
+
+#import <Cocoa/Cocoa.h>
+#import "AngbandAudio.h"
+#include "io/files-util.h"
+#include "main/music-definitions-table.h"
+#include "main/sound-definitions-table.h"
+#include "main/sound-of-music.h"
+#include "util/angband-files.h"
+#include <limits.h>
+#include <string.h>
+#include <ctype.h>
+
+
+/*
+ * Helper functions to extract the appropriate number ID from a name in
+ * music.cfg. Return true if the extraction failed and false if the extraction
+ * succeeded with *id holding the result.
+ */
+static bool get_basic_id(const char *s, int *id)
+{
+ int i = 0;
+
+ while (1) {
+ if (i >= MUSIC_BASIC_MAX) {
+ return true;
+ }
+ if (!strcmp(angband_music_basic_name[i], s)) {
+ *id = i;
+ return false;
+ }
+ ++i;
+ }
+}
+
+
+static bool get_dungeon_id(const char *s, int *id)
+{
+ if (strcmp(s, "dungeon") == 0) {
+ char *pe;
+ long lv = strtol(s + 7, &pe, 10);
+
+ if (pe != s && !*pe && lv > INT_MIN && lv < INT_MAX) {
+ *id = (int)lv;
+ return false;
+ }
+ }
+ return true;
+}
+
+
+static bool get_quest_id(const char *s, int *id)
+{
+ if (strcmp(s, "quest") == 0) {
+ char *pe;
+ long lv = strtol(s + 5, &pe, 10);
+
+ if (pe != s && !*pe && lv > INT_MIN && lv < INT_MAX) {
+ *id = (int)lv;
+ return false;
+ }
+ }
+ return true;
+}
+
+
+static bool get_town_id(const char *s, int *id)
+{
+ if (strcmp(s, "town") == 0) {
+ char *pe;
+ long lv = strtol(s + 4, &pe, 10);
+
+ if (pe != s && !*pe && lv > INT_MIN && lv < INT_MAX) {
+ *id = (int)lv;
+ return false;
+ }
+ }
+ return true;
+}
+
+
+static bool get_monster_id(const char *s, int *id)
+{
+ if (strcmp(s, "monster") == 0) {
+ char *pe;
+ long lv = strtol(s + 7, &pe, 10);
+
+ if (pe != s && !*pe && lv > INT_MIN && lv < INT_MAX) {
+ *id = (int)lv;
+ return false;
+ }
+ }
+ return true;
+}
+
+
+@implementation AngbandActiveAudio
+
+/*
+ * Handle property methods where need more than is provided by the default
+ * synthesis.
+ */
+- (BOOL) isPlaying
+{
+ return self->player && [self->player isPlaying];
+}
+
+
+- (id)initWithPlayer:(AVAudioPlayer *)aPlayer fadeInBy:(NSInteger)fadeIn
+ prior:(AngbandActiveAudio *)p paused:(BOOL)isPaused
+{
+ if (self = [super init]) {
+ self->_priorAudio = p;
+ if (p) {
+ self->_nextAudio = p->_nextAudio;
+ if (p->_nextAudio) {
+ p->_nextAudio->_priorAudio = self;
+ }
+ p->_nextAudio = self;
+ } else {
+ self->_nextAudio = nil;
+ }
+ self->player = aPlayer;
+ self->fadeTimer = nil;
+ if (aPlayer) {
+ aPlayer.delegate = self;
+ if (fadeIn > 0) {
+ float volume = [aPlayer volume];
+
+ aPlayer.volume = 0;
+ [aPlayer setVolume:volume
+ fadeDuration:(fadeIn * .001)];
+ }
+ if (!isPaused) {
+ [aPlayer play];
+ }
+ }
+ }
+ return self;
+}
+
+
+- (void)pause
+{
+ if (self->player) {
+ [self->player pause];
+ }
+}
+
+
+- (void)resume
+{
+ if (self->player) {
+ [self->player play];
+ }
+}
+
+
+- (void)stop
+{
+ if (self->player) {
+ if (self->fadeTimer) {
+ [self->fadeTimer invalidate];
+ self->fadeTimer = nil;
+ }
+ [self->player stop];
+ }
+}
+
+
+- (void)fadeOutBy:(NSInteger)t
+{
+ /* Only fade out if there's a track and it is not already fading out. */
+ if (self->player && !self->fadeTimer) {
+ [self->player setVolume:0.0f
+ fadeDuration:(((t > 0) ? t : 0) * .001)];
+ /* Set up a timer to remove the faded out track. */
+ self->fadeTimer = [NSTimer
+ scheduledTimerWithTimeInterval:(t * .001 + .01)
+ target:self
+ selector:@selector(handleFadeOutTimer:)
+ userInfo:nil
+ repeats:NO];
+ self->fadeTimer.tolerance = 0.02;
+ }
+}
+
+
+- (void)changeVolumeTo:(NSInteger)v
+{
+ if (self->player) {
+ NSInteger safev;
+
+ if (v < 0) {
+ safev = 0;
+ } else if (v > 100) {
+ safev = 100;
+ } else {
+ safev = v;
+ }
+ self->player.volume = safev * .01f;
+ }
+}
+
+
+- (void)handleFadeOutTimer:(NSTimer *)timer
+{
+ assert(self->player && self->fadeTimer == timer);
+ self->fadeTimer = nil;
+ [self->player stop];
+}
+
+
+- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)aPlayer
+ successfully:(BOOL)flag
+{
+ assert(aPlayer == self->player);
+ if (self->fadeTimer) {
+ [self->fadeTimer invalidate];
+ self->fadeTimer = nil;
+ }
+ self->player = nil;
+ /* Unlink from the list of active tracks. */
+ assert([self priorAudio] && [self nextAudio]);
+ self->_priorAudio->_nextAudio = self->_nextAudio;
+ self->_nextAudio->_priorAudio = self->_priorAudio;
+ self->_priorAudio = nil;
+ self->_nextAudio = nil;
+}
+
+@end
+
+
+@implementation AngbandAudioManager
+
+@synthesize soundVolume=_soundVolume;
+@synthesize musicEnabled=_musicEnabled;
+@synthesize musicVolume=_musicVolume;
+
+/*
+ * Handle property methods where need more than provided by the default
+ * synthesis.
+ */
+- (void)setSoundVolume:(NSInteger)v
+{
+ /* Incidental sounds that are currently playing aren't changed. */
+ if (v < 0) {
+ self->_soundVolume = 0;
+ } else if (v > 100) {
+ self->_soundVolume = 100;
+ } else {
+ self->_soundVolume = v;
+ }
+}
+
+
+- (void)setMusicEnabled:(BOOL)b
+{
+ BOOL old = self->_musicEnabled;
+
+ self->_musicEnabled = b;
+
+ if (!b && old) {
+ AngbandActiveAudio *a = [self->tracksPlayingHead nextAudio];
+
+ while (a) {
+ AngbandActiveAudio *t = a;
+
+ a = [a nextAudio];
+ [t stop];
+ };
+ }
+}
+
+
+- (void)setMusicVolume:(NSInteger)v
+{
+ NSInteger old = self->_musicVolume;
+
+ if (v < 0) {
+ self->_musicVolume = 0;
+ } else if (v > 100) {
+ self->_musicVolume = 100;
+ } else {
+ self->_musicVolume = v;
+ }
+
+ if (v != old) {
+ [[self->tracksPlayingTail priorAudio]
+ changeVolumeTo:self->_musicVolume];
+ }
+}
+
+
+/* Define methods. */
+- (id)init
+{
+ if (self = [super init]) {
+ self->tracksPlayingHead = [[AngbandActiveAudio alloc]
+ initWithPlayer:nil fadeInBy:0 prior:nil paused:NO];
+ self->tracksPlayingTail = [[AngbandActiveAudio alloc]
+ initWithPlayer:nil fadeInBy:0
+ prior:self->tracksPlayingHead paused:NO];
+ self->soundArraysByEvent = nil;
+ self->musicByTypeAndID = nil;
+ self->appActive = YES;
+ self->_beepEnabled = YES;
+ self->_soundEnabled = YES;
+ self->_soundVolume = 30;
+ self->_musicEnabled = YES;
+ self->_musicPausedWhenInactive = YES;
+ self->_musicVolume = 20;
+ self->_musicTransitionTime = 3000;
+ }
+ return self;
+}
+
+
+- (void)playBeep
+{
+ if (!self->appActive || ![self isBeepEnabled]) {
+ return;
+ }
+ /*
+ * Use NSBeep() for this, though that means it doesn't heed the
+ * volume set by the soundVolume property.
+ */
+ NSBeep();
+}
+
+- (void)playSound:(int)event
+{
+ if (!self->appActive || ![self isSoundEnabled]) {
+ return;
+ }
+
+ /* Initialize when the first sound is played. */
+ if (!self->soundArraysByEvent) {
+ self->soundArraysByEvent =
+ [AngbandAudioManager setupSoundArraysByEvent];
+ if (!self->soundArraysByEvent) {
+ return;
+ }
+ }
+
+ @autoreleasepool {
+ NSMutableArray *samples = [self->soundArraysByEvent
+ objectForKey:[NSNumber numberWithInteger:event]];
+ AVAudioPlayer *player;
+ int s;
+
+ if (!samples || !samples.count) {
+ return;
+ }
+
+ s = randint0((int)samples.count);
+ player = samples[s];
+
+ if ([player isPlaying]) {
+ [player stop];
+ player.currentTime = 0;
+ }
+ player.volume = self.soundVolume * .01f;
+ [player play];
+ }
+}
+
+
+- (void)playMusicType:(int)t ID:(int)i
+{
+ if (![self isMusicEnabled]) {
+ return;
+ }
+
+ /* Initialize when the first music track is played. */
+ if (!self->musicByTypeAndID) {
+ self->musicByTypeAndID =
+ [AngbandAudioManager setupMusicByTypeAndID];
+ }
+
+ @autoreleasepool {
+ NSMutableDictionary *musicByID = [self->musicByTypeAndID
+ objectForKey:[NSNumber numberWithInteger:t]];
+ NSMutableArray *paths;
+ NSData *audioData;
+ AVAudioPlayer *player;
+
+ if (!musicByID) {
+ return;
+ }
+ paths = [musicByID
+ objectForKey:[NSNumber numberWithInteger:i]];
+ if (!paths || !paths.count) {
+ return;
+ }
+ audioData = [NSData dataWithContentsOfFile:paths[randint0((int)paths.count)]];
+ player = [[AVAudioPlayer alloc] initWithData:audioData
+ error:nil];
+
+ if (player) {
+ AngbandActiveAudio *prior_track =
+ [self->tracksPlayingTail priorAudio];
+ AngbandActiveAudio *active_track;
+ NSInteger fade_time;
+
+ player.volume = 0.01f * [self musicVolume];
+ if ([prior_track isPlaying]) {
+ fade_time = [self musicTransitionTime];
+ if (fade_time < 0) {
+ fade_time = 0;
+ }
+ [prior_track fadeOutBy:fade_time];
+ } else {
+ fade_time = 0;
+ }
+ active_track = [[AngbandActiveAudio alloc]
+ initWithPlayer:player fadeInBy:fade_time
+ prior:prior_track
+ paused:(!self->appActive && [self isMusicPausedWhenInactive])];
+ }
+ }
+}
+
+
+- (BOOL)musicExists:(int)t ID:(int)i
+{
+ NSMutableDictionary *musicByID;
+ BOOL exists;
+
+ /* Initialize on the first call if it hasn't already been done. */
+ if (!self->musicByTypeAndID) {
+ self->musicByTypeAndID =
+ [AngbandAudioManager setupMusicByTypeAndID];
+ }
+
+ musicByID = [self->musicByTypeAndID
+ objectForKey:[NSNumber numberWithInteger:t]];
+
+ if (musicByID) {
+ NSString *path = [musicByID
+ objectForKey:[NSNumber numberWithInteger:i]];
+
+ exists = (path != nil);
+ } else {
+ exists = NO;
+ }
+
+ return exists;
+}
+
+
+- (void)stopAllMusic
+{
+ AngbandActiveAudio *track = [self->tracksPlayingHead nextAudio];
+
+ while (1) {
+ [track stop];
+ track = [track nextAudio];
+ if (!track) {
+ break;
+ }
+ }
+}
+
+
+- (void)setupForInactiveApp
+{
+ if (!self->appActive) {
+ return;
+ }
+ self->appActive = NO;
+ if ([self isMusicPausedWhenInactive]) {
+ AngbandActiveAudio *track =
+ [self->tracksPlayingHead nextAudio];
+
+ while (1) {
+ AngbandActiveAudio *next_track = [track nextAudio];
+
+ if (next_track != self->tracksPlayingTail) {
+ /* Stop all tracks but the last one playing. */
+ [track stop];
+ } else {
+ /*
+ * Pause the last track playing. Set its
+ * volume to maximum so, when resumed, it'll
+ * play that way even if it was fading in when
+ * paused.
+ */
+ [track pause];
+ [track changeVolumeTo:[self musicVolume]];
+ }
+ track = next_track;
+ if (!track) {
+ break;
+ }
+ }
+ }
+}
+
+
+- (void)setupForActiveApp
+{
+ if (self->appActive) {
+ return;
+ }
+ self->appActive = YES;
+ if ([self isMusicPausedWhenInactive]) {
+ /* Resume any tracks that were playing. */
+ AngbandActiveAudio *track =
+ [self->tracksPlayingTail priorAudio];
+
+ while (1) {
+ [track resume];
+ track = [track priorAudio];
+ if (!track) {
+ break;
+ }
+ }
+ }
+}
+
+
+/* Set up the class properties. */
+static NSInteger _maxSamples = 16;
++ (NSInteger)maxSamples
+{
+ return _maxSamples;
+}
+
+
+static AngbandAudioManager *_sharedManager = nil;
++ (AngbandAudioManager *)sharedManager
+{
+ if (!_sharedManager) {
+ _sharedManager = [[AngbandAudioManager alloc] init];
+ }
+ return _sharedManager;
+}
+
+
+/* Define class methods. */
++ (void)clearSharedManager
+{
+ _sharedManager = nil;
+}
+
+
++ (NSMutableDictionary *)setupSoundArraysByEvent
+{
+ char sound_dir[1024], path[1024];
+ FILE *fff;
+ NSMutableDictionary *arraysByEvent;
+
+ /* Build the "sound" path. */
+ path_build(sound_dir, sizeof(sound_dir), ANGBAND_DIR_XTRA, "sound");
+
+ /* Find and open the config file. */
+ path_build(path, sizeof(path), sound_dir, "sound.cfg");
+ fff = angband_fopen(path, "r");
+
+ if (!fff) {
+ NSLog(@"The sound configuration file could not be opened");
+ return nil;
+ }
+
+ arraysByEvent = [[NSMutableDictionary alloc] init];
+ @autoreleasepool {
+ /*
+ * This loop may take a while depending on the count and size
+ * of samples to load.
+ */
+ const char white[] = " \t";
+ NSMutableDictionary *playersByPath =
+ [[NSMutableDictionary alloc] init];
+ char buffer[2048];
+
+ /* Parse the file. */
+ /* Lines are always of the form "name = sample [sample ...]". */
+ while (angband_fgets(fff, buffer, sizeof(buffer)) == 0) {
+ NSMutableArray *soundSamples;
+ char *msg_name;
+ char *sample_name;
+ char *search;
+ int match;
+ size_t skip;
+
+ /* Skip leading whitespace. */
+ skip = strspn(buffer, white);
+
+ /*
+ * Ignore anything not beginning with an alphabetic
+ * character.
+ */
+ if (!buffer[skip] || !isalpha((unsigned char)buffer[skip])) {
+ continue;
+ }
+
+ /*
+ * Split the line into two; message name and the rest.
+ */
+ search = strchr(buffer + skip, '=');
+ if (!search) {
+ continue;
+ }
+ msg_name = buffer + skip;
+ skip = strcspn(msg_name, white);
+ if (skip > (size_t)(search - msg_name)) {
+ /*
+ * No white space between the message name and
+ * '='.
+ */
+ *search = '\0';
+ } else {
+ msg_name[skip] = '\0';
+ }
+ skip = strspn(search + 1, white);
+ sample_name = search + 1 + skip;
+
+ /* Make sure this is a valid event name. */
+ for (match = SOUND_MAX - 1; match >= 0; --match) {
+ if (!strcmp(msg_name, angband_sound_name[match])) {
+ break;
+ }
+ }
+ if (match < 0) {
+ continue;
+ }
+
+ soundSamples = [arraysByEvent
+ objectForKey:[NSNumber numberWithInteger:match]];
+ if (!soundSamples) {
+ soundSamples = [[NSMutableArray alloc] init];
+ [arraysByEvent
+ setObject:soundSamples
+ forKey:[NSNumber numberWithInteger:match]];
+ }
+
+ /*
+ * Now find all the sample names and add them one by
+ * one.
+ */
+ while (1) {
+ int num;
+ NSString *token_string;
+ AVAudioPlayer *player;
+ BOOL done;
+
+ if (!sample_name[0]) {
+ break;
+ }
+ /* Terminate the current token. */
+ skip = strcspn(sample_name, white);
+ done = !sample_name[skip];
+ sample_name[skip] = '\0';
+
+ /* Don't allow too many samples. */
+ num = (int) soundSamples.count;
+ if (num >= [AngbandAudioManager maxSamples]) {
+ break;
+ }
+
+ token_string = [NSString
+ stringWithUTF8String:sample_name];
+ player = [playersByPath
+ objectForKey:token_string];
+ if (!player) {
+ /*
+ * We have to load the sound.
+ * Build the path to the sample.
+ */
+ struct stat stb;
+
+ path_build(path, sizeof(path),
+ sound_dir, sample_name);
+ if (stat(path, &stb) == 0
+ && (stb.st_mode & S_IFREG)) {
+ NSData *audioData = [NSData
+ dataWithContentsOfFile:[NSString stringWithUTF8String:path]];
+
+ player = [[AVAudioPlayer alloc]
+ initWithData:audioData
+ error:nil];
+ if (player) {
+ [playersByPath
+ setObject:player
+ forKey:token_string];
+ }
+ }
+ }
+
+ /* Store it if it was loaded. */
+ if (player) {
+ [soundSamples addObject:player];
+ }
+
+ if (done) {
+ break;
+ }
+ sample_name += skip + 1;
+ skip = strspn(sample_name, white);
+ sample_name += skip;
+ }
+ }
+ playersByPath = nil;
+ }
+ angband_fclose(fff);
+
+ return arraysByEvent;
+}
+
+
++ (NSMutableDictionary *)setupMusicByTypeAndID
+{
+ struct {
+ const char * name;
+ int type_code;
+ bool *p_has;
+ bool (*name2id_func)(const char*, int*);
+ } sections_of_interest[] = {
+ { "Basic", TERM_XTRA_MUSIC_BASIC, NULL, get_basic_id },
+ { "Dungeon", TERM_XTRA_MUSIC_DUNGEON, NULL, get_dungeon_id },
+ { "Quest", TERM_XTRA_MUSIC_QUEST, NULL, get_quest_id },
+ { "Town", TERM_XTRA_MUSIC_TOWN, NULL, get_town_id },
+ { "Monster", TERM_XTRA_MUSIC_MONSTER, &has_monster_music,
+ get_monster_id },
+ { NULL, 0, NULL, NULL } /* terminating sentinel */
+ };
+ char music_dir[1024], path[1024];
+ FILE *fff;
+ NSMutableDictionary *catalog;
+
+ /* Build the "sound" path. */
+ path_build(music_dir, sizeof(music_dir), ANGBAND_DIR_XTRA, "music");
+
+ /* Find and open the config file. */
+ path_build(path, sizeof(path), music_dir, "music.cfg");
+ fff = angband_fopen(path, "r");
+
+ if (!fff) {
+ NSLog(@"The music configuration file could not be opened");
+ return nil;
+ }
+
+ catalog = [[NSMutableDictionary alloc] init];
+ @autoreleasepool {
+ const char white[] = " \t";
+ NSMutableDictionary *catalogTypeRestricted = nil;
+ int isec = -1;
+ char buffer[2048];
+
+ /* Parse the file. */
+ while (angband_fgets(fff, buffer, sizeof(buffer)) == 0) {
+ NSMutableArray *samples;
+ char *id_name;
+ char *sample_name;
+ char *search;
+ size_t skip;
+ int id;
+
+ /* Skip leading whitespace. */
+ skip = strspn(buffer, white);
+
+ /*
+ * Ignore empty lines or ones that only have comments.
+ */
+ if (!buffer[skip] || buffer[skip] == '#') {
+ continue;
+ }
+
+ if (buffer[skip] == '[') {
+ /*
+ * Found the start of a new section. Will do
+ * nothing if the section name is malformed
+ * (i.e. missing the trailing bracket).
+ */
+ search = strchr(buffer + skip + 1, ']');
+ if (search) {
+ isec = 0;
+
+ *search = '\0';
+ while (1) {
+ if (!sections_of_interest[isec].name) {
+ catalogTypeRestricted =
+ nil;
+ isec = -1;
+ break;
+ }
+ if (!strcmp(sections_of_interest[isec].name, buffer + skip + 1)) {
+ NSNumber *key = [NSNumber
+ numberWithInteger:sections_of_interest[isec].type_code];
+ catalogTypeRestricted =
+ [catalog objectForKey:key];
+ if (!catalogTypeRestricted) {
+ catalogTypeRestricted =
+ [[NSMutableDictionary alloc] init];
+ [catalog
+ setObject:catalogTypeRestricted
+ forKey:key];
+ }
+ break;
+ }
+ ++isec;
+ }
+ }
+ /* Skip the rest of the line. */
+ continue;
+ }
+
+ /*
+ * Targets should begin with an alphabetical character.
+ * Skip anything else.
+ */
+ if (!isalpha((unsigned char)buffer[skip])) {
+ continue;
+ }
+
+ search = strchr(buffer + skip, '=');
+ if (!search) {
+ continue;
+ }
+ id_name = buffer + skip;
+ skip = strcspn(id_name, white);
+ if (skip > (size_t)(search - id_name)) {
+ /*
+ * No white space between the name on the left
+ * and '='.
+ */
+ *search = '\0';
+ } else {
+ id_name[skip] = '\0';
+ }
+ skip = strspn(search + 1, white);
+ sample_name = search + 1 + skip;
+
+ if (!catalogTypeRestricted
+ || (*(sections_of_interest[isec].name2id_func))(id_name, &id)) {
+ /*
+ * It is not in a section of interest or did
+ * not recognize what was on the left side of
+ * '='. Ignore the line.
+ */
+ continue;
+ }
+ if (sections_of_interest[isec].p_has) {
+ *(sections_of_interest[isec].p_has) = true;
+ }
+ samples = [catalogTypeRestricted
+ objectForKey:[NSNumber numberWithInteger:id]];
+ if (!samples) {
+ samples = [[NSMutableArray alloc] init];
+ [catalogTypeRestricted
+ setObject:samples
+ forKey:[NSNumber numberWithInteger:id]];
+ }
+
+ /*
+ * Now find all the sample names and add them one by
+ * one.
+ */
+ while (1) {
+ BOOL done;
+ struct stat stb;
+
+ if (!sample_name[0]) {
+ break;
+ }
+ /* Terminate the current token. */
+ skip = strcspn(sample_name, white);
+ done = !sample_name[skip];
+ sample_name[skip] = '\0';
+
+ /*
+ * Check if the path actually corresponds to a
+ * file. Also restrict the number of samples
+ * stored for any type/ID combination.
+ */
+ path_build(path, sizeof(path), music_dir,
+ sample_name);
+ if (stat(path, &stb) == 0
+ && (stb.st_mode & S_IFREG)
+ && (int)samples.count
+ < [AngbandAudioManager maxSamples]) {
+ [samples addObject:[NSString
+ stringWithUTF8String:path]];
+ }
+
+ if (done) {
+ break;
+ }
+ sample_name += skip + 1;
+ skip = strspn(sample_name, white);
+ sample_name += skip;
+ }
+ }
+ }
+ angband_fclose(fff);
+
+ return catalog;
+}
+
+@end
*/
#import <Cocoa/Cocoa.h>
+#import "SoundAndMusic.h"
-@interface AngbandAppDelegate : NSObject <NSApplicationDelegate> {
+@interface AngbandAppDelegate : NSObject <NSApplicationDelegate,
+ SoundAndMusicChanges> {
NSMenu *_graphicsMenu;
NSMenu *_commandMenu;
NSDictionary *_commandMenuTagMap;
@property (nonatomic, retain) IBOutlet NSMenu *graphicsMenu;
@property (strong, nonatomic, retain) IBOutlet NSMenu *commandMenu;
@property (strong, nonatomic, retain) NSDictionary *commandMenuTagMap;
+@property (strong, nonatomic) SoundAndMusicPanelController
+ *soundAndMusicPanelController;
- (IBAction)newGame:(id)sender;
- (IBAction)editFont:(id)sender;
- (IBAction)openGame:(id)sender;
- (IBAction)saveGame:(id)sender;
- (IBAction)setRefreshRate:(NSMenuItem *)sender;
-- (IBAction)toggleSound:(NSMenuItem *)menuItem;
- (IBAction)toggleWideTiles:(NSMenuItem *)sender;
+- (IBAction)showSoundAndMusicPanel:(NSMenuItem *)sender;
- (void)setGraphicsMode:(NSMenuItem *)sender;
- (void)selectWindow:(id)sender;
- (void)beginGame;
* \brief This is a minimal implementation of the OS X front end.
*
* Use this file to rebuild the .nib file with Xcode without having to pull
- * in all of the Hengband source. This is the procedure with Xcode 12:
+ * in all of the Hengband source. This is the procedure with Xcode 14:
*
* 1) Create a new Xcode project for a macOS App.
* 2) You can set the "Product Name", "Team", "Organization Name",
* you can turn it off to avoid extra clutter.
* 3) In hengband's project settings on the "Info" tab, set the deployment
* target to what's used in Hengband's src/Makefile.am. When this was
- * written, that was 10.8 and 10.8 is necessary for Base localization.
- * In the localizations part of that tab, click the '+' and add a Japanese
- * localization. That will prompt you for the files involved. Leave that
- * as is: one file, "MainMenu.xib", with Base as the reference language
- * and localizable strings as the file type.
- * 4) In hengband's targets on the "General" tab, verify that "Main Interface"
- * is MainMenu.
- * 5) Copy src/cocoa/AppDelegate.h and src/cocoa/AppDelegate.m from the
+ * written, that was 10.13. As least 10.8 is necessary for Base
+ * localization. In the localizations part of that tab, click the '+' and
+ * add a Japanese localization. That will prompt you for the files
+ * involved. Leave that as is: one file, "MainMenu.xib", with Base as the
+ * reference language and localizable strings as the file type.
+ * 4) Copy src/cocoa/AppDelegate.h, src/cocoa/AppDelegate.m,
+ * src/cocoa/SoundAndMusic.h, and src/cocoa/SoundAndMusic.mm from the
* Hengband source files to the directory in the project with main.m. Copy
- * src/cocoa/Base.lproj/MainMenu.xib to the Base.lproj subdirectory of that
- * directory. Copy src/cocoa/ja.lproj/MainMenu.strings to the ja.lproj
- * subdirectory of that directory.
- * 6) (This annoyance seems to have gone away beween Xcode 11 and Xcode 13;
+ * src/cocoa/Base.lproj/MainMenu.xib and
+ * src/cocoa/Base.lproj/SoundAndMusic.xib to the Base.lproj subdirectory of
+ * that directory. Copy src/cocoa/ja.lproj/MainMenu.strings to the ja.lproj
+ * subdirectory of that directory. In Xcode, use "File->Add Files..." to
+ * add the copies of SoundAndMusic.h, SoundAndMusic.mm, and
+ * SoundAndMusic.xib to the project. In the file view in Xcode, click on
+ * SoundAndMusic.xib and in the Indentity and in the File inspector for that
+ * file, turn on Japanese localization for that file.
+ * 5) (This annoyance seems to have gone away beween Xcode 11 and Xcode 13;
* leaving it here just in case) If you modify MainMenu.xib after copying
* it over, you may want to set it so that it can be opened in older
* versions of Xcode. Select it in Xcode, and select one of the things,
* "Latest Xcode" will close the file and save it with the appropriate
* flags. Note that reopening the xib file in Xcode and saving it will
* cause the version to revert to the latest Xcode.
- * 7) If you want to change the Japanese strings for the menus, one way to
+ * 6) If you want to change the Japanese strings for the menus, one way to
* partly do it in Xcode is to export the localizations: from the file view
* select topmost category ("hengband" with an application icon) and then
- * select Editor->Export for Localization... in Xcode's menu bar. That
+ * select Product->Export Localizations... in Xcode's menu bar. That
* will prompt you for where to save the exported localizations. That
* export is done as a directory tree. Within it, you'll find a
* ja.xcloc/Localized Contents/ja.xliff file. The strings bracketed with
* Japanese version. Adjust the strings bracketed with <target></target>,
* save the modified file, and use Editor->Import Localizations... from
* within Xcode to import the localization from the ja.xloc directory.
- * The result of that will be to regenerate ja.lproj/MainMenu.strings in the
- * Xcode project files which you can use to replace the version in
- * src/cocoa/ja.lproj/MainMenu.strings in the Hengband source code.
- * 8) Use Xcode's Product->Build For->Running menu entry to build the project.
- * 9) The generated .nib file for English will be
- * Contents/Resources/Base.lproj/MainMenu.nib in the product directory which
- * is something like
+ * The result of that will be to regenerate ja.lproj/MainMenu.strings and
+ * ja.lproj/SoundAndMusic.strings in the Xcode project files which you can
+ * use to replace the versions in src/cocoa/ja.lproj/MainMenu.strings and
+ * src/cocoa/ja.lproj/SoundAndMusic.strings in the Hengband source code.
+ * 7) Use Xcode's Product->Build For->Running menu entry to build the project.
+ * 8) The generated .nib files for English will be
+ * Contents/Resources/Base.lproj/MainMenu.nib and
+ * Contents/Resources/Base.lproj/SoundAndMusic.nib in the product
+ * directory which is something like
* ~/Library/Developer/Xcode/DerivedData/<product_name>-<some_string>/Build/Products/Debug/<product_name>.app
- * You can use it to replace the src/cocoa/Base.lproj/MainMenu.nib in the
- * Hengband source files. With Xcode 13, the generated .nib files are
- * directories. In the build results from that version of Xcode,
+ * You can use those to replace src/cocoa/Base.lproj/MainMenu.nib and
+ * src/cocoa/Base.lproj/SoundAndMusic.nib in the Hengband source files.
+ * With Xcode 13 (though seemingly not in Xcode 14), the generated .nib
+ * files are directories. In the build results from that version of Xcode,
* copy Contents/Resources/Base.lproj/MainMenu.nib/keyedobjects.nib to
* replace src/cocoa/Base.lproj/MainMenu.nib in the Hengband source files
* (the keyedobjects-101300.nib file in the build results is for
@synthesize graphicsMenu=_graphicsMenu;
@synthesize commandMenu=_commandMenu;
@synthesize commandMenuTagMap=_comandMenuTagMap;
+@synthesize soundAndMusicPanelController=_soundAndMusicPanelController;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
- (IBAction)setRefreshRate:(NSMenuItem *)sender {
}
-- (IBAction)toggleSound:(NSMenuItem*)menuItem {
-}
-
- (IBAction)toggleWideTiles:(NSMenuItem *)sender {
}
- (IBAction)setGraphicsMode:(NSMenuItem *)sender {
}
+- (IBAction)showSoundAndMusicPanel:(NSMenuItem *)sender {
+ if (!self.soundAndMusicPanelController) {
+ self.soundAndMusicPanelController =
+ [[SoundAndMusicPanelController alloc]
+ initWithWindow:nil];
+ self.soundAndMusicPanelController.changeHandler = self;
+ }
+ [self.soundAndMusicPanelController showWindow:sender];
+}
+
- (void)selectWindow:(id)sender {
}
- (void)beginGame {
}
+- (void)changeSoundEnabled:(BOOL)newv {
+}
+
+- (void)changeSoundVolume:(NSInteger)newv {
+}
+
+- (void)changeMusicEnabled:(BOOL)newv {
+}
+
+- (void)changeMusicPausedWhenInactive:(BOOL)newv {
+}
+
+- (void)changeMusicVolume:(NSInteger)newv {
+}
+
+- (void)changeMusicTransitionTime:(NSInteger)newv {
+}
+
+- (void)soundAndMusicPanelWillClose {
+}
+
@end
<?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="15705" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
- <development version="8000" identifier="xcode"/>
- <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15705"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
</items>
</menu>
</menuItem>
- <menuItem title="Toggle Sound" state="on" id="mul-VV-UfU">
+ <menuItem title="Sound and Music ..." id="FdG-dF-c6Z" userLabel="Sound and Music ...">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
- <action selector="toggleSound:" target="Voe-Tx-rLC" id="y1f-OG-ExO"/>
+ <action selector="showSoundAndMusicPanel:" target="Voe-Tx-rLC" id="8zL-wI-O0R"/>
</connections>
</menuItem>
<menuItem title="Toggle Wide Tiles" state="on" id="tR9-z3-4SZ" userLabel="Toggle Wide Tiles">
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
+ <dependencies>
+ <deployment identifier="macosx"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <objects>
+ <customObject id="-2" userLabel="File's Owner" customClass="SoundAndMusicPanelController">
+ <connections>
+ <outlet property="musicEnabledControl" destination="yd0-H6-FT1" id="haI-My-g5n"/>
+ <outlet property="musicPausedWhenInactiveControl" destination="Z7a-h3-Z6B" id="08u-nG-2iz"/>
+ <outlet property="musicTransitionTimeControl" destination="AkU-q6-TkR" id="iwd-dc-xow"/>
+ <outlet property="musicVolumeControl" destination="r8d-9c-ObP" id="qf8-0E-lZp"/>
+ <outlet property="soundEnabledControl" destination="P10-1o-zNT" id="BVI-EV-7If"/>
+ <outlet property="soundVolumeControl" destination="JfF-pR-frk" id="NFw-u8-eL9"/>
+ <outlet property="window" destination="mVt-nk-Qo0" id="f7c-To-L8b"/>
+ </connections>
+ </customObject>
+ <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
+ <customObject id="-3" userLabel="Application" customClass="NSObject"/>
+ <window title="Sound and Music" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="mVt-nk-Qo0" customClass="NSPanel">
+ <windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES"/>
+ <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
+ <rect key="contentRect" x="120" y="64" width="265" height="232"/>
+ <rect key="screenRect" x="0.0" y="0.0" width="1280" height="775"/>
+ <view key="contentView" id="Nlk-Lb-y90">
+ <rect key="frame" x="0.0" y="0.0" width="265" height="234"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <subviews>
+ <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Kei-Lu-1AE">
+ <rect key="frame" x="5" y="211" width="113" height="16"/>
+ <textFieldCell key="cell" lineBreakMode="clipping" title="Incidental Sounds" id="aWu-Lm-bgH">
+ <font key="font" metaFont="system"/>
+ <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
+ </textFieldCell>
+ </textField>
+ <slider verticalHuggingPriority="750" id="JfF-pR-frk" userLabel="soundVolumeSlider">
+ <rect key="frame" x="115" y="155" width="132" height="28"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
+ <sliderCell key="cell" alignment="left" minValue="1" maxValue="100" doubleValue="30" tickMarkPosition="above" sliderType="linear" id="eKG-5Z-eGo"/>
+ <connections>
+ <action selector="respondToSoundVolumeSlider:" target="-2" id="FOm-uN-n61"/>
+ </connections>
+ </slider>
+ <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P10-1o-zNT" userLabel="soundEnabledCheckBox">
+ <rect key="frame" x="25" y="186" width="75" height="18"/>
+ <buttonCell key="cell" type="check" title="Enabled" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="u2I-E0-No1">
+ <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
+ <font key="font" metaFont="system"/>
+ </buttonCell>
+ <connections>
+ <action selector="respondToSoundEnabledToggle:" target="-2" id="1MV-lJ-Xs2"/>
+ </connections>
+ </button>
+ <button identifier="musicEnabled" verticalHuggingPriority="750" id="yd0-H6-FT1" userLabel="musicEnabledCheckBox">
+ <rect key="frame" x="25" y="95" width="92" height="22"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
+ <buttonCell key="cell" type="check" title="Enabled" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="Hur-85-rfj">
+ <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
+ <font key="font" metaFont="system"/>
+ </buttonCell>
+ <connections>
+ <action selector="respondToMusicEnabledToggle:" target="-2" id="sBT-EV-ENk"/>
+ </connections>
+ </button>
+ <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Z7a-h3-Z6B">
+ <rect key="frame" x="25" y="69" width="162" height="22"/>
+ <buttonCell key="cell" type="check" title="Paused when iconified" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="mXt-mL-aNz">
+ <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
+ <font key="font" metaFont="system"/>
+ </buttonCell>
+ <connections>
+ <action selector="respondToMusicPausedWhenInactiveToggle:" target="-2" id="JtQ-Gi-tad"/>
+ </connections>
+ </button>
+ <slider verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="r8d-9c-ObP" userLabel="musicVolumeSlider">
+ <rect key="frame" x="115" y="38" width="132" height="28"/>
+ <sliderCell key="cell" state="on" alignment="left" minValue="1" maxValue="100" doubleValue="20" tickMarkPosition="above" sliderType="linear" id="Xjg-wn-gAD"/>
+ <connections>
+ <action selector="respondToMusicVolumeSlider:" target="-2" id="5ed-hZ-UQO"/>
+ </connections>
+ </slider>
+ <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="2I9-zP-fUA">
+ <rect key="frame" x="25" y="22" width="71" height="16"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
+ <textFieldCell key="cell" lineBreakMode="clipping" title="Transitions" id="h6K-gp-brC">
+ <font key="font" metaFont="system"/>
+ <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
+ </textFieldCell>
+ </textField>
+ <textField horizontalHuggingPriority="251" verticalHuggingPriority="751" translatesAutoresizingMaskIntoConstraints="NO" id="B3i-gX-R4x" userLabel="musicVolumeLabel">
+ <rect key="frame" x="25" y="46" width="49" height="16"/>
+ <textFieldCell key="cell" lineBreakMode="clipping" title="Volume" id="bjo-lV-gRq" userLabel="musicVolumeLabelCell">
+ <font key="font" metaFont="system"/>
+ <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
+ </textFieldCell>
+ </textField>
+ <slider toolTip="amount of time to switch from one track to another" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="AkU-q6-TkR" userLabel="transitionTimeSlider">
+ <rect key="frame" x="115" y="14" width="132" height="28"/>
+ <sliderCell key="cell" state="on" alignment="left" maxValue="10000" doubleValue="3000" tickMarkPosition="above" sliderType="linear" id="8Li-wU-4rN"/>
+ <connections>
+ <action selector="respondToMusicTransitionTimeSlider:" target="-2" id="p66-ub-MQ8"/>
+ </connections>
+ </slider>
+ <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PS4-bM-MoX" userLabel="soundVolumeLabel">
+ <rect key="frame" x="25" y="163" width="49" height="16"/>
+ <textFieldCell key="cell" lineBreakMode="clipping" title="Volume" id="cYF-nO-lpm" userLabel="soundVolumeLabelCell">
+ <font key="font" metaFont="system"/>
+ <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
+ </textFieldCell>
+ </textField>
+ <textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="irV-Tu-qww">
+ <rect key="frame" x="5" y="122" width="116" height="16"/>
+ <textFieldCell key="cell" lineBreakMode="clipping" title="Background Music" id="uDO-28-V03">
+ <font key="font" metaFont="system"/>
+ <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
+ </textFieldCell>
+ </textField>
+ </subviews>
+ <constraints>
+ <constraint firstItem="Z7a-h3-Z6B" firstAttribute="top" secondItem="yd0-H6-FT1" secondAttribute="bottom" constant="6" symbolic="YES" id="1Fx-zh-MgX"/>
+ <constraint firstItem="r8d-9c-ObP" firstAttribute="trailing" secondItem="JfF-pR-frk" secondAttribute="trailing" id="1Sr-qB-xJV"/>
+ <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="irV-Tu-qww" secondAttribute="trailing" constant="20" symbolic="YES" id="3RA-93-Lum"/>
+ <constraint firstItem="2I9-zP-fUA" firstAttribute="top" secondItem="B3i-gX-R4x" secondAttribute="bottom" constant="8" symbolic="YES" id="4Xn-tz-Xxf"/>
+ <constraint firstItem="Z7a-h3-Z6B" firstAttribute="leading" secondItem="P10-1o-zNT" secondAttribute="leading" id="5qF-wu-rlZ"/>
+ <constraint firstItem="P10-1o-zNT" firstAttribute="leading" secondItem="Kei-Lu-1AE" secondAttribute="leading" constant="20" id="6Yb-ud-D6H"/>
+ <constraint firstItem="irV-Tu-qww" firstAttribute="top" relation="greaterThanOrEqual" secondItem="PS4-bM-MoX" secondAttribute="bottom" priority="600" constant="25" id="CPh-y4-7zH"/>
+ <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="JfF-pR-frk" secondAttribute="trailing" constant="-78" id="Ea1-o5-f6n"/>
+ <constraint firstItem="r8d-9c-ObP" firstAttribute="trailing" secondItem="AkU-q6-TkR" secondAttribute="trailing" id="MvH-5j-fjB"/>
+ <constraint firstItem="r8d-9c-ObP" firstAttribute="leading" secondItem="JfF-pR-frk" secondAttribute="leading" id="Rm8-rT-xet"/>
+ <constraint firstItem="Kei-Lu-1AE" firstAttribute="leading" secondItem="Nlk-Lb-y90" secondAttribute="leading" constant="7" id="Un1-Zm-mKq"/>
+ <constraint firstItem="AkU-q6-TkR" firstAttribute="centerY" secondItem="2I9-zP-fUA" secondAttribute="centerY" id="Vta-H6-ZLa"/>
+ <constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="AkU-q6-TkR" secondAttribute="bottom" constant="20" symbolic="YES" id="XJN-fL-iDT"/>
+ <constraint firstItem="2I9-zP-fUA" firstAttribute="leading" secondItem="P10-1o-zNT" secondAttribute="leading" id="dh3-4k-8rI"/>
+ <constraint firstItem="Kei-Lu-1AE" firstAttribute="top" secondItem="Nlk-Lb-y90" secondAttribute="top" constant="7" id="e3r-su-yAX"/>
+ <constraint firstItem="yd0-H6-FT1" firstAttribute="leading" secondItem="P10-1o-zNT" secondAttribute="leading" id="eN5-vD-Q0I"/>
+ <constraint firstItem="B3i-gX-R4x" firstAttribute="leading" secondItem="P10-1o-zNT" secondAttribute="leading" id="eO4-Mm-Sac"/>
+ <constraint firstItem="yd0-H6-FT1" firstAttribute="top" secondItem="irV-Tu-qww" secondAttribute="bottom" constant="6" id="eke-6I-DLg"/>
+ <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="P10-1o-zNT" secondAttribute="trailing" constant="20" symbolic="YES" id="emq-ZO-Kas"/>
+ <constraint firstItem="yd0-H6-FT1" firstAttribute="leading" secondItem="P10-1o-zNT" secondAttribute="leading" id="fOW-1U-cFo"/>
+ <constraint firstItem="P10-1o-zNT" firstAttribute="top" secondItem="Kei-Lu-1AE" secondAttribute="bottom" constant="8" symbolic="YES" id="gAp-bL-O4A"/>
+ <constraint firstItem="r8d-9c-ObP" firstAttribute="centerY" secondItem="B3i-gX-R4x" secondAttribute="centerY" id="h9X-kb-hGr"/>
+ <constraint firstItem="JfF-pR-frk" firstAttribute="centerY" secondItem="PS4-bM-MoX" secondAttribute="centerY" id="kU6-QK-HdF"/>
+ <constraint firstItem="PS4-bM-MoX" firstAttribute="top" secondItem="P10-1o-zNT" secondAttribute="bottom" constant="8" symbolic="YES" id="lZR-2z-q6K"/>
+ <constraint firstItem="AkU-q6-TkR" firstAttribute="leading" secondItem="JfF-pR-frk" secondAttribute="leading" id="m1M-dl-UP3"/>
+ <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="Kei-Lu-1AE" secondAttribute="trailing" constant="20" symbolic="YES" id="n1g-JZ-I2c"/>
+ <constraint firstItem="irV-Tu-qww" firstAttribute="leading" secondItem="Kei-Lu-1AE" secondAttribute="leading" id="q0n-E3-aOl"/>
+ <constraint firstItem="B3i-gX-R4x" firstAttribute="top" secondItem="Z7a-h3-Z6B" secondAttribute="bottom" constant="8" symbolic="YES" id="tHk-gR-ioR"/>
+ <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="Z7a-h3-Z6B" secondAttribute="trailing" constant="20" symbolic="YES" id="uN4-c7-IoR"/>
+ <constraint firstItem="AkU-q6-TkR" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="2I9-zP-fUA" secondAttribute="trailing" constant="20" id="v1y-OQ-C7k"/>
+ <constraint firstItem="r8d-9c-ObP" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="B3i-gX-R4x" secondAttribute="trailing" constant="20" id="vnr-Im-Q7Y"/>
+ <constraint firstItem="JfF-pR-frk" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="PS4-bM-MoX" secondAttribute="trailing" constant="20" id="vyO-0w-T2I"/>
+ <constraint firstItem="PS4-bM-MoX" firstAttribute="leading" secondItem="P10-1o-zNT" secondAttribute="leading" id="zuq-qh-7Bz"/>
+ </constraints>
+ </view>
+ <point key="canvasLocation" x="-343.5" y="-232"/>
+ </window>
+ </objects>
+</document>
--- /dev/null
+/**
+ * \file SoundAndMusc.h
+ * \brief Declare interface to sound and music configuration panel used by
+ * the OS X front end.
+ */
+
+#import <Cocoa/Cocoa.h>
+
+
+/**
+ * Declare a protocol to encapsulate changes to the settings for sounds and
+ * music.
+ */
+@protocol SoundAndMusicChanges
+- (void)changeSoundEnabled:(BOOL)newv;
+- (void)changeSoundVolume:(NSInteger)newv;
+- (void)changeMusicEnabled:(BOOL)newv;
+- (void)changeMusicPausedWhenInactive:(BOOL)newv;
+- (void)changeMusicVolume:(NSInteger)newv;
+- (void)changeMusicTransitionTime:(NSInteger)newv;
+- (void)soundAndMusicPanelWillClose;
+@end
+
+
+/**
+ * Declare a NSWindowController subclass to load the panel from the nib file.
+ */
+@interface SoundAndMusicPanelController : NSWindowController <NSWindowDelegate>
+
+/**
+ * Hold whether or not incidental sounds (and beeps) are played.
+ */
+@property (nonatomic, getter=isSoundEnabled) BOOL soundEnabled;
+
+/**
+ * Hold the volume for incidental sounds as a percentage (1 to 100) of the
+ * system volume.
+ */
+@property (nonatomic) NSInteger soundVolume;
+
+/**
+ * Hold whether or not background music is played.
+ */
+@property (nonatomic, getter=isMusicEnabled) BOOL musicEnabled;
+
+/**
+ * Hold whether or not currently playing music tracks are paused when the
+ * containing application becomes inactive.
+ */
+@property (nonatomic, getter=isMusicPausedWhenInactive)
+ BOOL musicPausedWhenInactive;
+
+/**
+ * Hold the volume for background music as a percentage (1 to 100) of the
+ * system volume.
+ */
+@property (nonatomic) NSInteger musicVolume;
+
+/**
+ * Hold the transition time in milliseconds for when a background music track
+ * is started when another background music track is already playing. If
+ * than or equal to zero, the current track is stopped and the new track
+ * starts playing at full volume without any extra delay.
+ */
+@property (nonatomic) NSInteger musicTransitionTime;
+
+/**
+ * Is the delegate that responds when one of the settings changes.
+ */
+@property (weak, nonatomic) id<SoundAndMusicChanges> changeHandler;
+
+/* These are implementation details. */
+@property (strong) IBOutlet NSPanel *window;
+@property (strong) IBOutlet NSButton *soundEnabledControl;
+@property (strong) IBOutlet NSSlider *soundVolumeControl;
+@property (strong) IBOutlet NSButton *musicEnabledControl;
+@property (strong) IBOutlet NSButton *musicPausedWhenInactiveControl;
+@property (strong) IBOutlet NSSlider *musicVolumeControl;
+@property (strong) IBOutlet NSSlider *musicTransitionTimeControl;
+
+@end
--- /dev/null
+/**
+ * \file SoundAndMusic.m
+ * \brief Define interface to sound and music configuration panel used by the
+ * OS X front end.
+ *
+ * Copyright (c) 2023 Eric Branlund
+ *
+ * This work is free software; you can redistribute it and/or modify it
+ * under the terms of either:
+ *
+ * a) the GNU General Public License as published by the Free Software
+ * Foundation, version 2, or
+ *
+ * b) the "Angband licence":
+ * This software may be copied and distributed for educational, research,
+ * and not for profit purposes provided that this copyright and statement
+ * are included in all such copies. Other copyrights may also apply.
+ */
+
+#import "SoundAndMusic.h"
+
+
+@implementation SoundAndMusicPanelController
+
+/*
+ * Don't use the implicit @synthesize since this property is declared in a
+ * superclass, NSWindowController.
+ */
+@dynamic window;
+
+
+/*
+ * Handle property methods where need more than is provided by the default
+ * synthesis.
+ */
+- (BOOL)isSoundEnabled
+{
+ return ([self soundEnabledControl]
+ && [self soundEnabledControl].state != NSOffState) ? YES : NO;
+}
+
+
+- (void)setSoundEnabled:(BOOL)v
+{
+ if ([self soundEnabledControl]) {
+ [self soundEnabledControl].state = (v) ? NSOnState : NSOffState;
+ }
+}
+
+
+- (NSInteger)soundVolume
+{
+ return ([self soundVolumeControl]) ?
+ [self soundVolumeControl].integerValue : 100;
+}
+
+
+- (void)setSoundVolume:(NSInteger)v
+{
+ if ([self soundVolumeControl]) {
+ if (v < 1) {
+ v = 1;
+ } else if (v > 100) {
+ v = 100;
+ }
+ [self soundVolumeControl].integerValue = v;
+ }
+}
+
+
+- (BOOL)isMusicEnabled
+{
+ return ([self musicEnabledControl]
+ && [self musicEnabledControl].state != NSOffState) ? YES : NO;
+}
+
+
+- (void)setMusicEnabled:(BOOL)v
+{
+ if ([self musicEnabledControl]) {
+ [self musicEnabledControl].state = (v) ? NSOnState : NSOffState;
+ }
+}
+
+
+- (BOOL)isMusicPausedWhenInactive
+{
+ return ([self musicPausedWhenInactiveControl]
+ && [self musicPausedWhenInactiveControl].state != NSOnState)
+ ? NO : YES;
+}
+
+
+- (void)setMusicPausedWhenInactive:(BOOL)v
+{
+ if ([self musicPausedWhenInactiveControl]) {
+ [self musicPausedWhenInactiveControl].state =
+ (v) ? NSOnState : NSOffState;
+ }
+}
+
+
+- (NSInteger)musicVolume
+{
+ return ([self musicVolumeControl]) ?
+ [self musicVolumeControl].integerValue : 100;
+}
+
+
+- (void)setMusicVolume:(NSInteger)v
+{
+ if ([self musicVolumeControl]) {
+ if (v < 1) {
+ v = 1;
+ } else if (v > 100) {
+ v = 100;
+ }
+ [self musicVolumeControl].integerValue = v;
+ }
+}
+
+
+- (NSInteger)musicTransitionTime
+{
+ return ([self musicTransitionTimeControl]) ?
+ [self musicTransitionTimeControl].integerValue : 0;
+}
+
+
+- (void)setMusicTransitionTime:(NSInteger)v
+{
+ if ([self musicTransitionTimeControl]) {
+ if (v < 0) {
+ v = 0;
+ } else if (v > [self musicTransitionTimeControl].maxValue) {
+ v = (NSInteger)([self musicTransitionTimeControl].maxValue);
+ }
+ [self musicTransitionTimeControl].integerValue = v;
+ }
+}
+
+
+/**
+ * Supply the NSWindowDelegate's windowWillClose method to relay the close
+ * message through the SoundAndMusicChanges protocol.
+ */
+- (void)windowWillClose:(NSNotification *)notification
+{
+ if ([self changeHandler]) {
+ [[self changeHandler] soundAndMusicPanelWillClose];
+ }
+}
+
+
+/**
+ * Override the NSWindowController property getter to get the appropriate nib
+ * file.
+ */
+- (NSString *)windowNibName
+{
+ return @"SoundAndMusic";
+}
+
+
+/**
+ * Override the NSWindowController function to set some attributes on the
+ * window.
+ */
+- (void)windowDidLoad
+{
+ [super windowDidLoad];
+ self.window.floatingPanel = YES;
+ self.window.becomesKeyOnlyIfNeeded = YES;
+ [self.window center];
+ self.window.delegate = self;
+}
+
+
+- (IBAction)respondToSoundEnabledToggle:(id)sender
+{
+ if ([self changeHandler]) {
+ [[self changeHandler] changeSoundEnabled:self.isSoundEnabled];
+ }
+}
+
+
+- (IBAction)respondToSoundVolumeSlider:(id)sender
+{
+ if ([self changeHandler]) {
+ [[self changeHandler] changeSoundVolume:self.soundVolume];
+ }
+}
+
+
+- (IBAction)respondToMusicEnabledToggle:(id)sender
+{
+ if ([self changeHandler]) {
+ [[self changeHandler] changeMusicEnabled:self.isMusicEnabled];
+ }
+}
+
+
+- (IBAction)respondToMusicPausedWhenInactiveToggle:(id)sender
+{
+ if ([self changeHandler]) {
+ [[self changeHandler] changeMusicPausedWhenInactive:self.isMusicPausedWhenInactive];
+ }
+}
+
+
+- (IBAction)respondToMusicVolumeSlider:(id)sender
+{
+ if ([self changeHandler]) {
+ [[self changeHandler] changeMusicVolume:self.musicVolume];
+ }
+}
+
+
+- (IBAction)respondToMusicTransitionTimeSlider:(id)sender
+{
+ if ([self changeHandler]) {
+ [[self changeHandler] changeMusicTransitionTime:self.musicTransitionTime];
+ }
+}
+
+@end
/* Class = "NSMenu"; title = "Help"; ObjectID = "F2S-fz-NVQ"; */
"F2S-fz-NVQ.title" = "ヘルプ";
+/* Class = "NSMenuItem"; title = "Sound and Music ..."; ObjectID = "FdG-dF-c6Z"; */
+"FdG-dF-c6Z.title" = "音と音楽…";
+
/* Class = "NSMenuItem"; title = "Hengband Help"; ObjectID = "FKE-Sm-Kum"; */
"FKE-Sm-Kum.title" = "変愚蛮怒 ヘルプ";
/* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */
"LE2-aR-0XJ.title" = "すべてを手前に移動";
-/* Class = "NSMenuItem"; title = "Toggle Sound"; ObjectID = "mul-VV-UfU"; */
-"mul-VV-UfU.title" = "音変える";
-
/* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */
"NMo-om-nkz.title" = "サービス";
--- /dev/null
+/* Class = "NSSlider"; ibShadowedToolTip = "amount of time to switch from one track to another"; ObjectID = "AkU-q6-TkR"; */
+"AkU-q6-TkR.ibShadowedToolTip" = "あるトラックから別のトラックに切り替える時間";
+
+/* Class = "NSTextFieldCell"; title = "Incidental Sounds"; ObjectID = "aWu-Lm-bgH"; */
+"aWu-Lm-bgH.title" = "付随音";
+
+/* Class = "NSTextFieldCell"; title = "Volume"; ObjectID = "bjo-lV-gRq"; */
+"bjo-lV-gRq.title" = "音量";
+
+/* Class = "NSTextFieldCell"; title = "Volume"; ObjectID = "cYF-nO-lpm"; */
+"cYF-nO-lpm.title" = "音量";
+
+/* Class = "NSTextFieldCell"; title = "Transitions"; ObjectID = "h6K-gp-brC"; */
+"h6K-gp-brC.title" = "移行間隔";
+
+/* Class = "NSButtonCell"; title = "Enabled"; ObjectID = "Hur-85-rfj"; */
+"Hur-85-rfj.title" = "活性化する";
+
+/* Class = "NSWindow"; title = "Sound and Music"; ObjectID = "mVt-nk-Qo0"; */
+"mVt-nk-Qo0.title" = "音と音楽";
+
+/* Class = "NSButtonCell"; title = "Paused when iconified"; ObjectID = "mXt-mL-aNz"; */
+"mXt-mL-aNz.title" = "アイコン化されたときに一時停止";
+
+/* Class = "NSButtonCell"; title = "Enabled"; ObjectID = "u2I-E0-No1"; */
+"u2I-E0-No1.title" = "活性化する";
+
+/* Class = "NSTextFieldCell"; title = "Background Music"; ObjectID = "uDO-28-V03"; */
+"uDO-28-V03.title" = "音楽";
+
#ifdef MACH_O_COCOA
/* Mac headers */
#import "cocoa/AppDelegate.h"
+#import "cocoa/SoundAndMusic.h"
+#import "cocoa/AngbandAudio.h"
//#include <Carbon/Carbon.h> /* For keycodes */
/* Hack - keycodes to enable compiling in macOS 10.14 */
#define kVK_Return 0x24
#include "term/term-color-types.h"
#include "view/display-messages.h"
#include "autopick/autopick-pref-processor.h"
+#include "main/scene-table.h"
#include "main/sound-definitions-table.h"
#include "util/angband-files.h"
#include "util/enum-converter.h"
static NSString * const AngbandGraphicsDefaultsKey = @"GraphicsID";
static NSString * const AngbandBigTileDefaultsKey = @"UseBigTiles";
static NSString * const AngbandFrameRateDefaultsKey = @"FramesPerSecond";
-static NSString * const AngbandSoundDefaultsKey = @"AllowSound";
+static NSString * const AngbandSoundEnabledDefaultsKey = @"AllowSound";
+static NSString * const AngbandSoundVolumeDefaultsKey = @"SoundVolume";
+static NSString * const AngbandMusicEnabledDefaultsKey = @"AllowMusic";
+static NSString * const AngbandMusicPausedWhenInactiveDefaultsKey =
+ @"MusicPausedWhenInactive";
+static NSString * const AngbandMusicVolumeDefaultsKey = @"MusicVolume";
+static NSString * const AngbandMusicTransitionTimeDefaultsKey =
+ @"MusicTransitionTime";
static NSInteger const AngbandWindowMenuItemTagBase = 1000;
static NSInteger const AngbandCommandMenuItemTagBase = 2000;
#endif
/**
- * Load sound effects based on sound.cfg within the xtra/sound directory;
- * bridge to Cocoa to use NSSound for simple loading and playback, avoiding
- * I/O latency by caching all sounds at the start. Inherits full sound
- * format support from Quicktime base/plugins.
- * pelpel favoured a plist-based parser for the future but .cfg support
- * improves cross-platform compatibility.
- */
-@interface AngbandSoundCatalog : NSObject {
-@private
- /**
- * Stores instances of NSSound keyed by path so the same sound can be
- * used for multiple events.
- */
- NSMutableDictionary *soundsByPath;
- /**
- * Stores arrays of NSSound keyed by event number.
- */
- NSMutableDictionary *soundArraysByEvent;
-}
-
-/**
- * If NO, then playSound effectively becomes a do nothing operation.
- */
-@property (getter=isEnabled) BOOL enabled;
-
-/**
- * Set up for lazy initialization in playSound(). Set enabled to NO.
- */
-- (id)init;
-
-/**
- * If self.enabled is YES and the given event has one or more sounds
- * corresponding to it in the catalog, plays one of those sounds, chosen at
- * random.
- */
-- (void)playSound:(int)event;
-
-/**
- * Impose an arbitrary limit on the number of possible samples per event.
- * Currently not declaring this as a class property for compatibility with
- * versions of Xcode prior to 8.
- */
-+ (int)maxSamples;
-
-/**
- * Return the shared sound catalog instance, creating it if it does not
- * exist yet. Currently not declaring this as a class property for
- * compatibility with versions of Xcode prior to 8.
- */
-+ (AngbandSoundCatalog*)sharedSounds;
-
-/**
- * Release any resources associated with shared sounds.
- */
-+ (void)clearSharedSounds;
-
-@end
-
-@implementation AngbandSoundCatalog
-
-- (id)init {
- if (self = [super init]) {
- self->soundsByPath = nil;
- self->soundArraysByEvent = nil;
- self->_enabled = NO;
- }
- return self;
-}
-
-- (void)playSound:(int)event {
- if (! self.enabled) {
- return;
- }
-
- /* Initialize when the first sound is played. */
- if (self->soundArraysByEvent == nil) {
- /* Build the "sound" path */
- char sound_dir[1024];
- path_build(sound_dir, sizeof(sound_dir), ANGBAND_DIR_XTRA, "sound");
-
- /* Find and open the config file */
- char path[1024];
- path_build(path, sizeof(path), sound_dir, "sound.cfg");
- FILE *fff = angband_fopen(path, "r");
-
- /* Handle errors */
- if (!fff) {
- NSLog(@"The sound configuration file could not be opened.");
- return;
- }
-
- self->soundsByPath = [[NSMutableDictionary alloc] init];
- self->soundArraysByEvent = [[NSMutableDictionary alloc] init];
- @autoreleasepool {
- /*
- * This loop may take a while depending on the count and size of
- * samples to load.
- */
-
- /* Parse the file */
- /* Lines are always of the form "name = sample [sample ...]" */
- char buffer[2048];
- while (angband_fgets(fff, buffer, sizeof(buffer)) == 0) {
- char *msg_name;
- char *cfg_sample_list;
- char *search;
- char *cur_token;
- char *next_token;
- int match;
-
- /* Skip anything not beginning with an alphabetic character */
- if (!buffer[0] || !isalpha((unsigned char)buffer[0])) continue;
-
- /* Split the line into two: message name, and the rest */
- search = strchr(buffer, ' ');
- cfg_sample_list = strchr(search + 1, ' ');
- if (!search) continue;
- if (!cfg_sample_list) continue;
-
- /* Set the message name, and terminate at first space */
- msg_name = buffer;
- search[0] = '\0';
-
- /* Make sure this is a valid event name */
- for (match = MSG_MAX - 1; match >= 0; match--) {
- if (strcmp(msg_name, angband_sound_name[match]) == 0)
- break;
- }
- if (match < 0) continue;
-
- /*
- * Advance the sample list pointer so it's at the beginning of
- * text.
- */
- cfg_sample_list++;
- if (!cfg_sample_list[0]) continue;
-
- /* Terminate the current token */
- cur_token = cfg_sample_list;
- search = strchr(cur_token, ' ');
- if (search) {
- search[0] = '\0';
- next_token = search + 1;
- } else {
- next_token = NULL;
- }
-
- /*
- * Now we find all the sample names and add them one by one
- */
- while (cur_token) {
- NSMutableArray *soundSamples =
- [self->soundArraysByEvent
- objectForKey:[NSNumber numberWithInteger:match]];
- if (soundSamples == nil) {
- soundSamples = [[NSMutableArray alloc] init];
- [self->soundArraysByEvent
- setObject:soundSamples
- forKey:[NSNumber numberWithInteger:match]];
- }
- int num = (int) soundSamples.count;
-
- /* Don't allow too many samples */
- if (num >= [AngbandSoundCatalog maxSamples]) break;
-
- NSString *token_string =
- [NSString stringWithUTF8String:cur_token];
- NSSound *sound =
- [self->soundsByPath objectForKey:token_string];
-
- if (! sound) {
- /*
- * We have to load the sound. Build the path to the
- * sample.
- */
- path_build(path, sizeof(path), sound_dir, cur_token);
- struct stat stb;
- if (stat(path, &stb) == 0) {
- /* Load the sound into memory */
- sound = [[NSSound alloc]
- initWithContentsOfFile:[NSString stringWithUTF8String:path]
- byReference:YES];
- if (sound) {
- [self->soundsByPath setObject:sound
- forKey:token_string];
- }
- }
- }
-
- /* Store it if we loaded it */
- if (sound) {
- [soundSamples addObject:sound];
- }
-
- /* Figure out next token */
- cur_token = next_token;
- if (next_token) {
- /* Try to find a space */
- search = strchr(cur_token, ' ');
-
- /*
- * If we can find one, terminate, and set new "next".
- */
- if (search) {
- search[0] = '\0';
- next_token = search + 1;
- } else {
- /* Otherwise prevent infinite looping */
- next_token = NULL;
- }
- }
- }
- }
- }
- /* Close the file */
- angband_fclose(fff);
- }
-
- @autoreleasepool {
- NSMutableArray *samples =
- [self->soundArraysByEvent
- objectForKey:[NSNumber numberWithInteger:event]];
-
- if (samples == nil || samples.count == 0) {
- return;
- }
-
- /* Choose a random event. */
- int s = randint0((int) samples.count);
- NSSound *sound = samples[s];
-
- if ([sound isPlaying])
- [sound stop];
-
- /* Play the sound. */
- [sound play];
- }
-}
-
-+ (int)maxSamples {
- return 16;
-}
-
-/**
- * For sharedSounds and clearSharedSounds.
- */
-static __strong AngbandSoundCatalog* gSharedSounds = nil;
-
-+ (AngbandSoundCatalog*)sharedSounds {
- if (gSharedSounds == nil) {
- gSharedSounds = [[AngbandSoundCatalog alloc] init];
- }
- return gSharedSounds;
-}
-
-+ (void)clearSharedSounds {
- gSharedSounds = nil;
-}
-
-@end
-
-/**
* Each location in the terminal either stores a character, a tile,
* padding for a big tile, or padding for a big character (for example a
* kanji that takes two columns). These structures represent that. Note
static void prepare_paths_and_directories(void);
static void load_prefs(void);
static void init_windows(void);
-static void play_sound(int event);
static void send_key(const char key);
static BOOL check_events(int wait);
static BOOL send_event(NSEvent *event);
[self resizeTerminalWithContentRect: contentRect saveToDefaults: NO];
}
+- (void)windowWillMiniaturize:(NSNotification *)notification
+{
+ NSWindow *window = [notification object];
+
+ if (window != self.primaryWindow
+ || (__bridge AngbandContext*) (angband_terms[0]->data) != self)
+ {
+ return;
+ }
+ [[AngbandAudioManager sharedManager] setupForInactiveApp];
+}
+
+- (void)windowDidDeminiaturize:(NSNotification *)notification
+{
+ NSWindow *window = [notification object];
+
+ if (window != self.primaryWindow
+ || (__bridge AngbandContext*) (angband_terms[0]->data) != self)
+ {
+ return;
+ }
+ [[AngbandAudioManager sharedManager] setupForActiveApp];
+}
+
- (void)windowDidBecomeMain:(NSNotification *)notification
{
NSWindow *window = [notification object];
switch (n) {
/* Make a noise */
case TERM_XTRA_NOISE:
- NSBeep();
+ [[AngbandAudioManager sharedManager] playBeep];
break;
/* Make a sound */
case TERM_XTRA_SOUND:
- play_sound(v);
+ [[AngbandAudioManager sharedManager] playSound:v];
+ break;
+
+ /* Start playing background music. */
+ case TERM_XTRA_MUSIC_BASIC:
+ case TERM_XTRA_MUSIC_DUNGEON:
+ case TERM_XTRA_MUSIC_QUEST:
+ case TERM_XTRA_MUSIC_TOWN:
+ case TERM_XTRA_MUSIC_MONSTER:
+ [[AngbandAudioManager sharedManager] playMusicType:n ID:v];
break;
+ case TERM_XTRA_MUSIC_MUTE:
+ [[AngbandAudioManager sharedManager] stopAllMusic];
+ break;
+
+ case TERM_XTRA_SCENE:
+ {
+ auto &list = get_scene_type_list(v);
+
+ for (auto &item : list) {
+ if (![[AngbandAudioManager sharedManager]
+ musicExists:item.type ID:item.val]) {
+ continue;
+ }
+ [[AngbandAudioManager sharedManager]
+ playMusicType:item.type ID:item.val];
+ }
+ }
+ break;
+
/* Process random events */
case TERM_XTRA_BORED:
/*
term_nuke(angband_terms[i]);
}
}
- [AngbandSoundCatalog clearSharedSounds];
+ [AngbandAudioManager clearSharedManager];
[AngbandContext setDefaultFont:nil];
plog(str);
exit(0);
FallbackFontName, @"FontName-0",
[NSNumber numberWithFloat:FallbackFontSizeMain], @"FontSize-0",
[NSNumber numberWithInt:60], AngbandFrameRateDefaultsKey,
- [NSNumber numberWithBool:YES], AngbandSoundDefaultsKey,
+ [NSNumber numberWithBool:YES], AngbandSoundEnabledDefaultsKey,
+ [NSNumber numberWithInt:30], AngbandSoundVolumeDefaultsKey,
+ [NSNumber numberWithBool:YES], AngbandMusicEnabledDefaultsKey,
+ [NSNumber numberWithBool:YES], AngbandMusicPausedWhenInactiveDefaultsKey,
+ [NSNumber numberWithInt:20], AngbandMusicVolumeDefaultsKey,
+ [NSNumber numberWithInt:3000], AngbandMusicTransitionTimeDefaultsKey,
[NSNumber numberWithInt:GRAPHICS_NONE], AngbandGraphicsDefaultsKey,
[NSNumber numberWithBool:YES], AngbandBigTileDefaultsKey,
defaultTerms, AngbandTerminalsDefaultsKey,
}
/* Use sounds; set the Angband global */
- if ([defs boolForKey:AngbandSoundDefaultsKey]) {
+ if ([defs boolForKey:AngbandSoundEnabledDefaultsKey]) {
use_sound = true;
- [AngbandSoundCatalog sharedSounds].enabled = YES;
+ [AngbandAudioManager sharedManager].beepEnabled = YES;
+ [AngbandAudioManager sharedManager].soundEnabled = YES;
} else {
use_sound = false;
- [AngbandSoundCatalog sharedSounds].enabled = NO;
+ [AngbandAudioManager sharedManager].beepEnabled = NO;
+ [AngbandAudioManager sharedManager].soundEnabled = NO;
+ }
+ [AngbandAudioManager sharedManager].soundVolume =
+ [defs integerForKey:AngbandSoundVolumeDefaultsKey];
+
+ /* Use music; set the Angband global */
+ if ([defs boolForKey:AngbandMusicEnabledDefaultsKey]) {
+ use_music = true;
+ [AngbandAudioManager sharedManager].musicEnabled = YES;
+ } else {
+ use_music = false;
+ [AngbandAudioManager sharedManager].musicEnabled = NO;
}
+ [AngbandAudioManager sharedManager].musicPausedWhenInactive =
+ [defs boolForKey:AngbandMusicPausedWhenInactiveDefaultsKey];
+ [AngbandAudioManager sharedManager].musicVolume =
+ [defs integerForKey:AngbandMusicVolumeDefaultsKey];
+ [AngbandAudioManager sharedManager].musicTransitionTime =
+ [defs integerForKey:AngbandMusicTransitionTimeDefaultsKey];
/* fps */
frames_per_second = [defs integerForKey:AngbandFrameRateDefaultsKey];
}
/**
- * Play sound effects asynchronously. Select a sound from any available
- * for the required event, and bridge to Cocoa to play it.
- */
-static void play_sound(int event)
-{
- [[AngbandSoundCatalog sharedSounds] playSound:event];
-}
-
-/**
* Allocate the primary Angband terminal and activate it. Allocate the other
* Angband terminals.
*/
/* Set up game event handlers */
/* init_display(); */
- /* Register the sound hook */
- /* sound_hook = play_sound; */
-
/* Initialize some save file stuff */
p_ptr->player_euid = geteuid();
p_ptr->player_egid = getegid();
return (!game_in_progress
|| (w_ptr->character_generated && inkey_flag)) ? YES : NO;
}
- else if( sel == @selector(toggleSound:) )
- {
- BOOL is_on = [[NSUserDefaults standardUserDefaults]
- boolForKey:AngbandSoundDefaultsKey];
-
- [menuItem setState: ((is_on) ? NSOnState : NSOffState)];
- return YES;
- }
else if (sel == @selector(toggleWideTiles:)) {
BOOL is_on = [[NSUserDefaults standardUserDefaults]
boolForKey:AngbandBigTileDefaultsKey];
[context saveWindowVisibleToDefaults: YES];
}
-- (IBAction) toggleSound: (NSMenuItem *) sender
-{
- BOOL is_on = (sender.state == NSOnState);
-
- /* Toggle the state and update the Angband global and preferences. */
- if (is_on) {
- sender.state = NSOffState;
- use_sound = false;
- [AngbandSoundCatalog sharedSounds].enabled = NO;
- } else {
- sender.state = NSOnState;
- use_sound = true;
- [AngbandSoundCatalog sharedSounds].enabled = YES;
- }
- [[NSUserDefaults angbandDefaults] setBool:(! is_on)
- forKey:AngbandSoundDefaultsKey];
-}
-
- (IBAction)toggleWideTiles:(NSMenuItem *) sender
{
BOOL is_on = (sender.state == NSOnState);
}
}
+- (IBAction)showSoundAndMusicPanel:(NSMenuItem *)sender
+{
+ if (!self.soundAndMusicPanelController) {
+ self.soundAndMusicPanelController =
+ [[SoundAndMusicPanelController alloc] initWithWindow:nil];
+ } else {
+ self.soundAndMusicPanelController.changeHandler = nil;
+ }
+ self.soundAndMusicPanelController.soundEnabled =
+ [AngbandAudioManager sharedManager].isSoundEnabled;
+ self.soundAndMusicPanelController.soundVolume =
+ [AngbandAudioManager sharedManager].soundVolume;
+ self.soundAndMusicPanelController.musicEnabled =
+ [AngbandAudioManager sharedManager].isMusicEnabled;
+ self.soundAndMusicPanelController.musicPausedWhenInactive =
+ [AngbandAudioManager sharedManager].isMusicPausedWhenInactive;
+ self.soundAndMusicPanelController.musicVolume =
+ [AngbandAudioManager sharedManager].musicVolume;
+ self.soundAndMusicPanelController.musicTransitionTime =
+ [AngbandAudioManager sharedManager].musicTransitionTime;
+ [self.soundAndMusicPanelController showWindow:sender];
+ self.soundAndMusicPanelController.changeHandler = self;
+}
+
+/*
+ * Implement the SoundAndMusicChanges protocol to respond to changes from
+ * the Sound and Music dialog.
+ */
+- (void)changeSoundEnabled:(BOOL)newv
+{
+ use_sound = (newv) ? true : false;
+ [AngbandAudioManager sharedManager].beepEnabled = newv;
+ [AngbandAudioManager sharedManager].soundEnabled = newv;
+ [[NSUserDefaults angbandDefaults] setBool:newv
+ forKey:AngbandSoundEnabledDefaultsKey];
+}
+
+- (void)changeSoundVolume:(NSInteger)newv
+{
+ [AngbandAudioManager sharedManager].soundVolume = newv;
+ [[NSUserDefaults angbandDefaults] setInteger:newv
+ forKey:AngbandSoundVolumeDefaultsKey];
+}
+
+- (void)changeMusicEnabled:(BOOL)newv
+{
+ use_music = (newv) ? true : false;
+ [AngbandAudioManager sharedManager].musicEnabled = newv;
+ [[NSUserDefaults angbandDefaults] setBool:newv
+ forKey:AngbandMusicEnabledDefaultsKey];
+}
+
+- (void)changeMusicPausedWhenInactive:(BOOL)newv
+{
+ [AngbandAudioManager sharedManager].musicPausedWhenInactive = newv;
+ [[NSUserDefaults angbandDefaults] setBool:newv
+ forKey:AngbandMusicPausedWhenInactiveDefaultsKey];
+}
+
+- (void)changeMusicVolume:(NSInteger)newv
+{
+ [AngbandAudioManager sharedManager].musicVolume = newv;
+ [[NSUserDefaults angbandDefaults] setInteger:newv
+ forKey:AngbandMusicVolumeDefaultsKey];
+}
+
+- (void)changeMusicTransitionTime:(NSInteger)newv
+{
+ [AngbandAudioManager sharedManager].musicTransitionTime = newv;
+ [[NSUserDefaults angbandDefaults] setInteger:newv
+ forKey:AngbandMusicTransitionTimeDefaultsKey];
+}
+
+- (void)soundAndMusicPanelWillClose
+{
+ AngbandContext *mainWindow =
+ (__bridge AngbandContext*) (angband_terms[0]->data);
+
+ [mainWindow.primaryWindow makeKeyAndOrderFront: nil];
+ self.soundAndMusicPanelController.changeHandler = nil;
+}
+
- (void)prepareWindowsMenu
{
@autoreleasepool {
}
}
+- (void)applicationDidBecomeActive:(NSNotification *)notification
+{
+ [[AngbandAudioManager sharedManager] setupForActiveApp];
+}
+
+- (void)applicationWillResignActive:(NSNotification *)notification
+{
+ [[AngbandAudioManager sharedManager] setupForInactiveApp];
+}
+
- (void)awakeFromNib
{
[super awakeFromNib];