OSDN Git Service

Mac: implement background music
authorEric Branlund <ebranlund@fastmail.com>
Tue, 18 Apr 2023 15:07:36 +0000 (09:07 -0600)
committerbackwardsEric <55062581+backwardsEric@users.noreply.github.com>
Thu, 20 Apr 2023 03:44:01 +0000 (21:44 -0600)
Resolves https://github.com/backwardsEric/hengband/issues/28 .

14 files changed:
src/Makefile.am
src/cocoa/AngbandAudio.h [new file with mode: 0644]
src/cocoa/AngbandAudio.mm [new file with mode: 0644]
src/cocoa/AppDelegate.h
src/cocoa/AppDelegate.m
src/cocoa/Base.lproj/MainMenu.nib
src/cocoa/Base.lproj/MainMenu.xib
src/cocoa/Base.lproj/SoundAndMusic.nib [new file with mode: 0644]
src/cocoa/Base.lproj/SoundAndMusic.xib [new file with mode: 0644]
src/cocoa/SoundAndMusic.h [new file with mode: 0644]
src/cocoa/SoundAndMusic.mm [new file with mode: 0644]
src/cocoa/ja.lproj/MainMenu.strings
src/cocoa/ja.lproj/SoundAndMusic.strings [new file with mode: 0644]
src/main-cocoa.mm

index fc16977..d4a0602 100644 (file)
@@ -1048,7 +1048,8 @@ EXTRA_hengband_SOURCES = \
 
 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 \
@@ -1059,13 +1060,15 @@ cocoa_plist_strings_template = cocoa/Angband-Cocoa.strings
 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
@@ -1086,11 +1089,15 @@ hengband_SOURCES += \
        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
@@ -1130,7 +1137,10 @@ EXTRA_hengband_SOURCES += main-cocoa.mm system/grafmode.h system/grafmode.cpp \
 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) \
diff --git a/src/cocoa/AngbandAudio.h b/src/cocoa/AngbandAudio.h
new file mode 100644 (file)
index 0000000..61aa5ac
--- /dev/null
@@ -0,0 +1,210 @@
+/**
+ * \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 */
diff --git a/src/cocoa/AngbandAudio.mm b/src/cocoa/AngbandAudio.mm
new file mode 100644 (file)
index 0000000..c786606
--- /dev/null
@@ -0,0 +1,905 @@
+/**
+ * \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
index b22c240..5f04faf 100644 (file)
  */
 
 #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;
index 586e1b9..306ac67 100644 (file)
@@ -3,7 +3,7 @@
  * \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
@@ -84,6 +91,7 @@
 @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
index c5e9e7d..2e43fe3 100644 (file)
Binary files a/src/cocoa/Base.lproj/MainMenu.nib and b/src/cocoa/Base.lproj/MainMenu.nib differ
index 715b874..8ffb260 100644 (file)
@@ -1,9 +1,8 @@
 <?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">
diff --git a/src/cocoa/Base.lproj/SoundAndMusic.nib b/src/cocoa/Base.lproj/SoundAndMusic.nib
new file mode 100644 (file)
index 0000000..0adb94c
Binary files /dev/null and b/src/cocoa/Base.lproj/SoundAndMusic.nib differ
diff --git a/src/cocoa/Base.lproj/SoundAndMusic.xib b/src/cocoa/Base.lproj/SoundAndMusic.xib
new file mode 100644 (file)
index 0000000..c10035b
--- /dev/null
@@ -0,0 +1,165 @@
+<?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>
diff --git a/src/cocoa/SoundAndMusic.h b/src/cocoa/SoundAndMusic.h
new file mode 100644 (file)
index 0000000..f9fa685
--- /dev/null
@@ -0,0 +1,81 @@
+/**
+ * \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
diff --git a/src/cocoa/SoundAndMusic.mm b/src/cocoa/SoundAndMusic.mm
new file mode 100644 (file)
index 0000000..9d972f9
--- /dev/null
@@ -0,0 +1,226 @@
+/**
+ * \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
index c29b5da..99d412d 100644 (file)
@@ -34,6 +34,9 @@
 /* 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" = "変愚蛮怒 ヘルプ";
 
@@ -61,9 +64,6 @@
 /* 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" = "サービス";
 
diff --git a/src/cocoa/ja.lproj/SoundAndMusic.strings b/src/cocoa/ja.lproj/SoundAndMusic.strings
new file mode 100644 (file)
index 0000000..55c333a
--- /dev/null
@@ -0,0 +1,30 @@
+/* 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" = "音楽";
+
index 497a2bc..23d160d 100644 (file)
@@ -23,6 +23,8 @@
 #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
@@ -53,6 +55,7 @@
 #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"
@@ -74,7 +77,14 @@ static NSString * const AngbandTerminalVisibleDefaultsKey = @"Visible";
 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;
 
@@ -111,268 +121,6 @@ static wchar_t convert_two_byte_eucjp_to_utf32_native(const char *cp);
 #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
@@ -2171,7 +1919,6 @@ static NSString* AngbandCorrectedDirectoryPath(NSString *originalPath);
 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);
@@ -3949,6 +3696,30 @@ static int compare_nsrect_yorigin_greater(const void *ap, const void *bp)
     [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];
@@ -4672,14 +4443,42 @@ static errr Term_xtra_cocoa(int n, int v)
        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:
            /*
@@ -5408,7 +5207,7 @@ static void hook_quit(const char * str)
             term_nuke(angband_terms[i]);
         }
     }
-    [AngbandSoundCatalog clearSharedSounds];
+    [AngbandAudioManager clearSharedManager];
     [AngbandContext setDefaultFont:nil];
     plog(str);
     exit(0);
@@ -5638,7 +5437,12 @@ static void load_prefs(void)
                               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,
@@ -5657,13 +5461,32 @@ static void load_prefs(void)
     }
 
     /* 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];
@@ -5688,15 +5511,6 @@ static void load_prefs(void)
 }
 
 /**
- * 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.
  */
@@ -5890,9 +5704,6 @@ static void init_windows(void)
        /* 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();
@@ -6026,14 +5837,6 @@ static void init_windows(void)
         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];
@@ -6101,24 +5904,6 @@ static void init_windows(void)
     [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);
@@ -6137,6 +5922,88 @@ static void init_windows(void)
     }
 }
 
+- (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 {
@@ -6272,6 +6139,16 @@ static void init_windows(void)
     }
 }
 
+- (void)applicationDidBecomeActive:(NSNotification *)notification
+{
+    [[AngbandAudioManager sharedManager] setupForActiveApp];
+}
+
+- (void)applicationWillResignActive:(NSNotification *)notification
+{
+    [[AngbandAudioManager sharedManager] setupForInactiveApp];
+}
+
 - (void)awakeFromNib
 {
     [super awakeFromNib];