> With sounds on, using #levelchange to drop more than one level only > gave the level-loss sound effect once. Fix the underlying issue within macsound.m by allowing the currently playing sound to run to its completion prior to starting the next. This also backs out the acknowledgment --More-- that was added after the sound issue first materialized and puts it back to the way it was the past several releases.
388 lines
12 KiB
Objective-C
388 lines
12 KiB
Objective-C
/* macsound.m */
|
|
/* Copyright Michael Allison, 2023 */
|
|
/* NetHack may be freely redistributed. See license for details. */
|
|
|
|
|
|
/*
|
|
* macsound
|
|
*/
|
|
|
|
#include "hack.h"
|
|
|
|
#ifdef DEBUG
|
|
#undef DEBUG
|
|
#endif
|
|
|
|
/* #import <Foundation/Foundation.h> */
|
|
#import <AppKit/AppKit.h>
|
|
|
|
#define SPEECH_SOUNDS
|
|
#ifdef SPEECH_SOUNDS
|
|
#import <AVFoundation/AVFoundation.h>
|
|
#endif
|
|
|
|
static void macsound_init_nhsound(void);
|
|
static void macsound_exit_nhsound(const char *);
|
|
static void macsound_achievement(schar, schar, int32_t);
|
|
static void macsound_soundeffect(char *, int32_t, int32_t);
|
|
static void macsound_hero_playnotes(int32_t, const char *, int32_t);
|
|
static void macsound_play_usersound(char *, int32_t, int32_t);
|
|
static void macsound_ambience(int32_t, int32_t, int32_t);
|
|
static void macsound_verbal(char *, int32_t, int32_t, int32_t, int32_t);
|
|
static int affiliate(int32_t seid, const char *soundname);
|
|
|
|
|
|
|
|
/*
|
|
* Sound capabilities that can be enabled:
|
|
* SOUND_TRIGGER_USERSOUNDS | SOUND_TRIGGER_HEROMUSIC
|
|
* | SOUND_TRIGGER_ACHIEVEMENTS | SOUND_TRIGGER_SOUNDEFFECTS
|
|
* | SOUND_TRIGGER_AMBIENCE,
|
|
*/
|
|
|
|
struct sound_procs macsound_procs = {
|
|
SOUNDID(macsound),
|
|
SOUND_TRIGGER_HEROMUSIC | SOUND_TRIGGER_SOUNDEFFECTS
|
|
| SOUND_TRIGGER_ACHIEVEMENTS
|
|
#ifdef SND_SPEECH
|
|
| SOUND_TRIGGER_VERBAL
|
|
#endif
|
|
|0,
|
|
macsound_init_nhsound,
|
|
macsound_exit_nhsound,
|
|
macsound_achievement,
|
|
macsound_soundeffect,
|
|
macsound_hero_playnotes,
|
|
macsound_play_usersound,
|
|
macsound_ambience,
|
|
macsound_verbal,
|
|
};
|
|
|
|
static void
|
|
macsound_init_nhsound(void)
|
|
{
|
|
/* Initialize external sound library */
|
|
}
|
|
|
|
static void
|
|
macsound_exit_nhsound(const char *reason UNUSED)
|
|
{
|
|
/* Close / Terminate external sound library */
|
|
|
|
}
|
|
|
|
static void
|
|
macsound_ambience(int32_t ambienceid UNUSED, int32_t ambience_action UNUSED,
|
|
int32_t hero_proximity UNUSED)
|
|
{
|
|
}
|
|
|
|
/* magic number 47 is the current number of sound_ files to include */
|
|
#define EXTRA_SOUNDS 47
|
|
|
|
static int32_t affiliation[number_of_se_entries + number_of_sa2_entries + EXTRA_SOUNDS] = { 0 };
|
|
static NSString *soundstring[number_of_se_entries + number_of_sa2_entries + EXTRA_SOUNDS];
|
|
static NSSound *seSound[number_of_se_entries + number_of_sa2_entries + EXTRA_SOUNDS];
|
|
|
|
#ifdef SND_SOUNDEFFECTS_AUTOMAP
|
|
#define AUTOMAPONLY
|
|
#else
|
|
#define AUTOMAPONLY UNUSED
|
|
#endif
|
|
|
|
/* fulfill SOUND_TRIGGER_SOUNDEFFECTS */
|
|
static void
|
|
macsound_soundeffect(char *desc UNUSED, int32_t seid AUTOMAPONLY, int vol AUTOMAPONLY)
|
|
{
|
|
#ifdef SND_SOUNDEFFECTS_AUTOMAP
|
|
|
|
/* Supposedly, the following locations are searched in this order:
|
|
* 1. the application's main bundle
|
|
* 2. ~/Library/Sounds
|
|
* 3. /Library/Sounds
|
|
* 4. /Network/Library/Sounds
|
|
* 5. /System/Library/Sounds
|
|
*/
|
|
|
|
char buf[1024];
|
|
const char *soundname;
|
|
float fvolume = (float) vol / 100.00;
|
|
|
|
if (fvolume < 0.1 || fvolume > 1.0)
|
|
fvolume = 1.0;
|
|
|
|
if (seid <= se_zero_invalid || seid >= number_of_se_entries)
|
|
return;
|
|
if (!affiliation[seid]) {
|
|
soundname = get_sound_effect_filename(seid, buf, sizeof buf,
|
|
sff_base_only);
|
|
if (soundname) {
|
|
affiliate(seid, soundname);
|
|
}
|
|
}
|
|
if (affiliation[seid]) {
|
|
if ([seSound[seid] isPlaying])
|
|
[seSound[seid] stop];
|
|
if ([seSound[seid] volume] != fvolume)
|
|
[seSound[seid] setVolume:fvolume];
|
|
[seSound[seid] play];
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#define WAVEMUSIC_SOUNDS
|
|
|
|
#ifdef WAVEMUSIC_SOUNDS
|
|
#define WAVEMUSICONLY
|
|
#else
|
|
#define WAVEMUSICONLY UNUSED
|
|
#endif
|
|
|
|
/* This is the number of sound_ files that support WAVEMUSIC_SOUNDS */
|
|
static const int wavemusic_sound_count = EXTRA_SOUNDS;
|
|
|
|
/* fulfill SOUND_TRIGGER_HEROMUSIC */
|
|
static void macsound_hero_playnotes(int32_t instrument WAVEMUSICONLY,
|
|
const char *str WAVEMUSICONLY, int32_t vol WAVEMUSICONLY)
|
|
{
|
|
#ifdef WAVEMUSIC_SOUNDS
|
|
uint32_t pseudo_seid, pseudo_seid_base = 0;
|
|
BOOL has_note_variations = false;
|
|
char resourcename[120], *end_of_res = 0;
|
|
const char *c = 0;
|
|
float fvolume = (float) vol / 100.00;
|
|
int time_remaining = 0;
|
|
NSTimeInterval dur;
|
|
NSTimeInterval elapsed;
|
|
|
|
if (fvolume < 0.1 || fvolume > 1.0)
|
|
fvolume = 1.0;
|
|
|
|
if (!str)
|
|
return;
|
|
resourcename[0] = '\0';
|
|
switch(instrument) {
|
|
case ins_tinkle_bell:
|
|
Strcpy(resourcename, "sound_Bell");
|
|
pseudo_seid_base = 0;
|
|
break;
|
|
case ins_taiko_drum: /* DRUM_OF_EARTHQUAKE */
|
|
Strcpy(resourcename, "sound_Drum_Of_Earthquake");
|
|
pseudo_seid_base = 1;
|
|
break;
|
|
case ins_baritone_sax: /* FIRE_HORN */
|
|
Strcpy(resourcename, "sound_Fire_Horn");
|
|
pseudo_seid_base = 2;
|
|
break;
|
|
case ins_french_horn: /* FROST_HORN */
|
|
Strcpy(resourcename, "sound_Frost_Horn");
|
|
pseudo_seid_base = 3;
|
|
break;
|
|
case ins_melodic_tom: /* LEATHER_DRUM */
|
|
Strcpy(resourcename, "sound_Leather_Drum");
|
|
pseudo_seid_base = 4;
|
|
break;
|
|
case ins_trumpet: /* BUGLE */
|
|
Strcpy(resourcename, "sound_Bugle");
|
|
has_note_variations = true;
|
|
pseudo_seid_base = 5;
|
|
break;
|
|
case ins_cello: /* MAGIC_HARP */
|
|
Strcpy(resourcename, "sound_Magic_Harp");
|
|
has_note_variations = true;
|
|
pseudo_seid_base = 12;
|
|
break;
|
|
case ins_english_horn: /* TOOLED_HORN */
|
|
Strcpy(resourcename, "sound_Tooled_Horn");
|
|
has_note_variations = true;
|
|
pseudo_seid_base = 19;
|
|
break;
|
|
case ins_flute: /* WOODEN_FLUTE */
|
|
Strcpy(resourcename, "sound_Wooden_Flute");
|
|
has_note_variations = true;
|
|
pseudo_seid_base = 26;
|
|
break;
|
|
case ins_orchestral_harp: /* WOODEN_HARP */
|
|
Strcpy(resourcename, "sound_Wooden_Harp");
|
|
has_note_variations = true;
|
|
pseudo_seid_base = 33;
|
|
break;
|
|
case ins_pan_flute: /* MAGIC_FLUTE */
|
|
Strcpy(resourcename, "sound_Magic_Flute");
|
|
has_note_variations = true;
|
|
pseudo_seid_base = 40;
|
|
break;
|
|
}
|
|
pseudo_seid_base += number_of_se_entries; /* get past se_ entries */
|
|
|
|
int i, idx = 0, notecount = strlen(str);
|
|
static const char *const note_suffixes[]
|
|
= { "_A", "_B", "_C", "_D", "_E", "_F", "_G" };
|
|
|
|
end_of_res = eos(resourcename);
|
|
c = str;
|
|
for (i = 0; i < notecount; ++i) {
|
|
if (*c >= 'A' && *c <= 'G') {
|
|
pseudo_seid = pseudo_seid_base;
|
|
if (has_note_variations) {
|
|
idx = (*c) - 'A';
|
|
pseudo_seid += idx;
|
|
if (pseudo_seid >= SIZE(affiliation))
|
|
break;
|
|
Strcpy(end_of_res, note_suffixes[idx]);
|
|
}
|
|
if (!affiliation[pseudo_seid]) {
|
|
affiliate(pseudo_seid, resourcename);
|
|
}
|
|
if (affiliation[pseudo_seid]) {
|
|
if ([seSound[pseudo_seid] isPlaying]) {
|
|
dur = [seSound[pseudo_seid] duration];
|
|
elapsed = [seSound[pseudo_seid] currentTime];
|
|
time_remaining = (int) ((dur - elapsed) * 1000);
|
|
msleep(time_remaining);
|
|
}
|
|
if ([seSound[pseudo_seid] isPlaying])
|
|
[seSound[pseudo_seid] stop];
|
|
if ([seSound[pseudo_seid] volume] != fvolume)
|
|
[seSound[pseudo_seid] setVolume:fvolume];
|
|
[seSound[pseudo_seid] play];
|
|
#if 0
|
|
if (i < notecount - 1) {
|
|
/* more notes to follow */
|
|
msleep(150);
|
|
[seSound[pseudo_seid] stop];
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
c++;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#define ACHIEVEMENT_SOUNDS
|
|
|
|
#ifdef ACHIEVEMENT_SOUNDS
|
|
#define ACHIEVEONLY
|
|
#else
|
|
#define ACHIEVEONLY UNUSED
|
|
#endif
|
|
|
|
/* fulfill SOUND_TRIGGER_ACHIEVEMENTS */
|
|
static void
|
|
macsound_achievement(schar ach1 ACHIEVEONLY,
|
|
schar ach2 ACHIEVEONLY,
|
|
int32_t moreinfo UNUSED)
|
|
{
|
|
#ifdef ACHIEVEMENT_SOUNDS
|
|
char resourcename[120];
|
|
uint32_t pseudo_seid, pseudo_seid_base;
|
|
int time_remaining = 0;
|
|
NSTimeInterval dur;
|
|
NSTimeInterval elapsed;
|
|
|
|
if (ach1 == 0 && ach2 == 0)
|
|
return;
|
|
|
|
resourcename[0] = '\0';
|
|
pseudo_seid_base = 0;
|
|
if (ach1 == 0) {
|
|
int sa2 = (int) ach2;
|
|
|
|
if (sa2 > sa2_zero_invalid && sa2 < number_of_sa2_entries) {
|
|
switch(sa2) {
|
|
case sa2_splashscreen:
|
|
Strcpy(resourcename, "sa2_splashscreen");
|
|
break;
|
|
case sa2_newgame_nosplash:
|
|
Strcpy(resourcename, "sa2_newgame_nosplash");
|
|
break;
|
|
case sa2_restoregame:
|
|
Strcpy(resourcename, "sa2_restoregame");
|
|
break;
|
|
case sa2_xplevelup:
|
|
Strcpy(resourcename, "sa2_xplevelup");
|
|
break;
|
|
case sa2_xpleveldown:
|
|
Strcpy(resourcename, "sa2_xpleveldown");
|
|
break;
|
|
}
|
|
if (resourcename[0] == '\0')
|
|
return;
|
|
|
|
/* get past se_ entries and the wavemusic entries */
|
|
pseudo_seid_base += (number_of_se_entries + wavemusic_sound_count);
|
|
|
|
pseudo_seid = pseudo_seid_base + sa2;
|
|
if (!affiliation[pseudo_seid]) {
|
|
affiliate(pseudo_seid, resourcename);
|
|
}
|
|
if (affiliation[pseudo_seid]) {
|
|
if ([seSound[pseudo_seid] isPlaying]) {
|
|
dur = [seSound[pseudo_seid] duration];
|
|
elapsed = [seSound[pseudo_seid] currentTime];
|
|
time_remaining = (int) ((dur - elapsed) * 1000);
|
|
msleep(time_remaining);
|
|
}
|
|
if ([seSound[pseudo_seid] isPlaying]) {
|
|
[seSound[pseudo_seid] stop];
|
|
}
|
|
[seSound[pseudo_seid] play];
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/* fulfill SOUND_TRIGGER_USERSOUNDS */
|
|
static void
|
|
macsound_play_usersound(char *filename UNUSED, int volume UNUSED, int idx UNUSED)
|
|
{
|
|
|
|
}
|
|
|
|
#ifdef SND_SPEECH
|
|
#define SPEECHONLY
|
|
#else
|
|
#define SPEECHONLY UNUSED
|
|
#endif
|
|
|
|
static void
|
|
macsound_verbal(char *text SPEECHONLY, int32_t gndr SPEECHONLY, int32_t tone_of_voice UNUSED, int32_t vol UNUSED, int32_t moreinfo UNUSED)
|
|
{
|
|
#ifdef SND_SPEECH
|
|
NSMutableString *speechstring;
|
|
AVSpeechUtterance *text2speechutt;
|
|
AVSpeechSynthesizer *synthesizer;
|
|
|
|
synthesizer = [[AVSpeechSynthesizer alloc]init];
|
|
speechstring = [NSMutableString stringWithUTF8String:text];
|
|
text2speechutt = [AVSpeechUtterance speechUtteranceWithString:speechstring];
|
|
text2speechutt.rate = 0.50f;
|
|
text2speechutt.pitchMultiplier = 0.8f;
|
|
text2speechutt.postUtteranceDelay = 0.2f;
|
|
text2speechutt.volume = (float) vol / 100;
|
|
if (gndr > 0)
|
|
text2speechutt.voice = [AVSpeechSynthesisVoice voiceWithIdentifier:@"com.apple.ttsbundle.siri_female_en-GB_compact"];
|
|
else
|
|
text2speechutt.voice = [AVSpeechSynthesisVoice voiceWithIdentifier:@"com.apple.ttsbundle.siri_male_en-GB_compact"];
|
|
|
|
[synthesizer speakUtterance:text2speechutt];
|
|
#endif /* SND_SPEECH */
|
|
}
|
|
|
|
static int
|
|
affiliate(int32_t id, const char *soundname)
|
|
{
|
|
if (!soundname || id <= se_zero_invalid || id >= SIZE(affiliation))
|
|
return 0;
|
|
|
|
if (!affiliation[id]) {
|
|
affiliation[id] = id;
|
|
soundstring[id] = [NSString stringWithUTF8String:soundname];
|
|
seSound[id] = [NSSound soundNamed:soundstring[id]];
|
|
}
|
|
return 1;
|
|
}
|
|
/* end of macsound.m */
|