From f53184d8f7001bbb8981814073c19d4d2ae5630d Mon Sep 17 00:00:00 2001 From: Nunuhara Cabbage Date: Sat, 2 Oct 2021 10:42:17 -0700 Subject: [PATCH] Implement System40.ini/AliceStart.ini mixers Implement "VolumeValancer" mixer channels from the .ini file. Mixers are now recursive, with the master and voice channels being parent nodes (this seems to be hardcoded in system 4, or at least I haven't found where the hierarchy is specified). Also implement .wai file support (needed for mixer channel info on sound files). --- include/mixer.h | 15 +++ include/sts_mixer.h | 5 +- include/xsystem4.h | 2 + src/audio_meta.c | 59 ++++++++++++ src/audio_mixer.c | 200 ++++++++++++++++++++++++++++++++++++---- src/hll/SystemService.c | 53 ++--------- src/system4.c | 7 ++ 7 files changed, 275 insertions(+), 66 deletions(-) diff --git a/include/mixer.h b/include/mixer.h index 68b29fc..3889e77 100644 --- a/include/mixer.h +++ b/include/mixer.h @@ -28,10 +28,25 @@ struct bgi { int volume; }; +struct wai { + //int uk1; + //int uk2; + int channel; + //int uk3; // version 4+ +}; + void bgi_read(const char *path); struct bgi *bgi_get(int no); +void wai_load(const char *path); +struct wai *wai_get(int no); void mixer_init(void); +int mixer_get_numof(void); +const char *mixer_get_name(int n); +int mixer_get_volume(int n, int *volume); +int mixer_set_volume(int n, int volume); +int mixer_get_mute(int n, int *mute); +int mixer_set_mute(int n, int mute); struct channel; enum asset_type; diff --git a/include/sts_mixer.h b/include/sts_mixer.h index 7336eed..8e27d64 100644 --- a/include/sts_mixer.h +++ b/include/sts_mixer.h @@ -341,8 +341,9 @@ void sts_mixer_mix_audio(sts_mixer_t* mixer, void* output, unsigned int samples) } // write to buffer - left = sts_mixer__clamp_sample(left); - right = sts_mixer__clamp_sample(right); + // NOTE: xsystem4 change: use mixer gain (not sure why this isn't implemented upstream...) + left = sts_mixer__clamp_sample(left * mixer->gain); + right = sts_mixer__clamp_sample(right * mixer->gain); switch (mixer->audio_format) { case STS_MIXER_SAMPLE_FORMAT_8: *out_8++ = (char)(left * 127.0f); diff --git a/include/xsystem4.h b/include/xsystem4.h index eb66711..a626379 100644 --- a/include/xsystem4.h +++ b/include/xsystem4.h @@ -30,6 +30,8 @@ struct config { int view_height; size_t mixer_nr_channels; char **mixer_channels; + int *mixer_volumes; + int default_volume; char *bgi_path; char *wai_path; diff --git a/src/audio_meta.c b/src/audio_meta.c index 0b9e32f..0dc316c 100644 --- a/src/audio_meta.c +++ b/src/audio_meta.c @@ -15,8 +15,11 @@ */ #include +#include #include "system4.h" +#include "system4/file.h" #include "mixer.h" +#include "little_endian.h" #define BGI_MAX 100 @@ -91,3 +94,59 @@ struct bgi *bgi_get(int no) } return NULL; } + +static struct wai *wai_data = NULL; +static int nr_wai = 0; + +void wai_load(const char *path) +{ + size_t len; + uint8_t *file = file_read(path, &len); + + if (len < 48 || file[0] != 'X' || file[1] != 'I' || file[2] != '2' || file[3] != '\0') { + WARNING("Not a .wai file: %s", path); + goto end; + } + int count = LittleEndian_getDW(file, 8) - 1; + if (count < 0) { + WARNING("Invalid .wai count: %d", count); + goto end; + } + int version = LittleEndian_getDW(file, 12); + if (version < 3 || version > 4) { + WARNING("Unsupported .wai version: %d", version); + goto end; + } + + uint8_t *data; + int record_size; + if (version == 4) { + data = file + 40; + record_size = 4*4; + } else { + data = file + 36; + record_size = 3*4; + } + + if (len - (version == 4 ? 40 : 36) < (size_t)count*record_size) { + WARNING(".wai file truncated"); + goto end; + } + + wai_data = xcalloc(count, sizeof(struct wai)); + for (int i = 0; i < count; i++) { + wai_data[i].channel = LittleEndian_getDW(data, i*record_size + 8); + } + nr_wai = count; + +end: + free(file); +} + +struct wai *wai_get(int no) +{ + no--; + if (no < 0 || no >= nr_wai) + return NULL; + return &wai_data[no]; +} diff --git a/src/audio_mixer.c b/src/audio_mixer.c index 8a6cd16..8e5126e 100644 --- a/src/audio_mixer.c +++ b/src/audio_mixer.c @@ -28,6 +28,8 @@ #include "mixer.h" #include "xsystem4.h" +#define clamp(min_value, max_value, value) min(max_value, max(min_value, value)) + /* * The actual mixer implementation is contained in this header. * The rest of this file implements a high level interface for managing mixer @@ -51,6 +53,7 @@ struct channel { // archive data struct archive_data *dfile; int no; + int mixer_no; // audio file data SNDFILE *file; @@ -73,15 +76,34 @@ struct channel { struct fade fade; }; +struct mixer { + sts_mixer_t mixer; + sts_mixer_stream_t stream; + int voice; + atomic_bool muted; + float data[CHUNK_SIZE * 2]; + char *name; + + struct mixer *parent; + struct mixer **children; + int nr_children; +}; + +static struct mixer *master = NULL; +static struct mixer *mixers = NULL; +static int nr_mixers = 0; + static SDL_AudioDeviceID audio_device = 0; -static sts_mixer_t mixer; /* * The SDL2 audio callback. */ static void audio_callback(possibly_unused void *data, Uint8 *stream, int len) { - sts_mixer_mix_audio(&mixer, stream, len / (sizeof(float) * 2)); + sts_mixer_mix_audio(&master->mixer, stream, len / (sizeof(float) * 2)); + if (master->muted) { + memset(stream, 0, len); + } } /* @@ -122,7 +144,6 @@ static bool cb_loop(struct channel *ch) * Callback to refill the stream's audio data. * Called from sys_mixer_mix_audio. */ -//static int refill_stream(sts_mixer_sample_t *sample, void *data) static int cb_read_frames(struct channel *ch, float *out, sf_count_t frame_count) { sf_count_t num_read = 0; @@ -210,7 +231,7 @@ static int refill_stream(sts_mixer_sample_t *sample, void *data) // set gain for fade if (ch->fade.fading) { float gain = cb_calc_fade(&ch->fade, ch->frame); - mixer.voices[ch->voice].gain = gain; + mixers[ch->mixer_no].mixer.voices[ch->voice].gain = gain; ch->volume = gain * 100.0; if (ch->frame >= ch->fade.end_pos) { @@ -224,6 +245,17 @@ static int refill_stream(sts_mixer_sample_t *sample, void *data) return r; } +static int refill_mixer(sts_mixer_sample_t *sample, void *data) +{ + struct mixer *mixer = data; + sts_mixer_mix_audio(&mixer->mixer, &mixer->data, CHUNK_SIZE); + if (mixer->muted) { + memset(mixer->data, 0, sizeof(float) * sample->length); + } + + return STS_STREAM_CONTINUE; +} + int channel_play(struct channel *ch) { SDL_LockAudioDevice(audio_device); @@ -231,7 +263,7 @@ int channel_play(struct channel *ch) SDL_UnlockAudioDevice(audio_device); return 0; } - ch->voice = sts_mixer_play_stream(&mixer, &ch->stream, 1.0f); + ch->voice = sts_mixer_play_stream(&mixers[ch->mixer_no].mixer, &ch->stream, 1.0f); SDL_UnlockAudioDevice(audio_device); return 1; } @@ -243,7 +275,7 @@ int channel_stop(struct channel *ch) SDL_UnlockAudioDevice(audio_device); return 0; } - sts_mixer_stop_voice(&mixer, ch->voice); + sts_mixer_stop_voice(&mixers[ch->mixer_no].mixer, ch->voice); ch->voice = -1; SDL_UnlockAudioDevice(audio_device); return 1; @@ -291,7 +323,7 @@ int channel_fade(struct channel *ch, int time, int volume, bool stop) ch->fade.start_pos = ch->frame; ch->fade.start_volume = (float)ch->volume / 100.0; ch->fade.end_pos = ch->fade.start_pos + ((time * ch->info.samplerate) / 1000); - ch->fade.end_volume = min(1.0, max(0.0, (float)volume / 100.0)); + ch->fade.end_volume = clamp(0.0f, 1.0f, (float)volume / 100.0f); SDL_UnlockAudioDevice(audio_device); return 1; } @@ -395,7 +427,7 @@ static sf_count_t channel_vio_seek(sf_count_t offset, int whence, void *data) ch->offset = ch->dfile->size + offset; break; } - ch->offset = min((sf_count_t)ch->dfile->size, max(0, ch->offset)); + ch->offset = clamp(0, (sf_count_t)ch->dfile->size, ch->offset); return ch->offset; } @@ -459,17 +491,28 @@ struct channel *channel_open(enum asset_type type, int no) ch->stream.sample.data = ch->data; ch->voice = -1; - struct bgi *bgi = bgi_get(no); - if (bgi) { - ch->volume = min(100, max(0, bgi->volume)); - ch->loop_start = min(ch->info.frames, max(0, bgi->loop_start)); - ch->loop_end = min(ch->info.frames, max(0, bgi->loop_end)); - ch->loop_count = max(0, bgi->loop_count); - } else { + if (type == ASSET_SOUND) { + struct wai *wai = wai_get(no); ch->volume = 100; ch->loop_start = 0; ch->loop_end = ch->info.frames; - ch->loop_count = type == ASSET_SOUND ? 1 : 0; + ch->loop_count = 1; + ch->mixer_no = wai ? wai->channel : 1; + } else { + struct bgi *bgi = bgi_get(no); + if (bgi) { + ch->volume = clamp(0, 100, bgi->volume); + ch->loop_start = clamp(0, ch->info.frames, bgi->loop_start); + ch->loop_end = clamp(0, ch->info.frames, bgi->loop_end); + ch->loop_count = max(0, bgi->loop_count); + ch->mixer_no = clamp(0, nr_mixers, bgi->channel); + } else { + ch->volume = 100; + ch->loop_start = 0; + ch->loop_end = ch->info.frames; + ch->loop_count = 0; + ch->mixer_no = 0; + } } ch->no = no; @@ -490,8 +533,81 @@ void channel_close(struct channel *ch) free(ch); } +#define SJIS_MASTER "\x83\x7d\x83\x58\x83\x5e\x81\x5b" +#define SJIS_VOICE "\x89\xb9\x90\xba" + void mixer_init(void) { + // initialize mixer naming + if (!config.mixer_nr_channels) { + nr_mixers = 3; + mixers = xcalloc(nr_mixers, sizeof(struct mixer)); + mixers[0].name = "Music"; + mixers[1].name = "Sound"; + mixers[2].name = "Master"; + } else { + // NOTE: Older games don't have an explicit master channel. + // We add an extra mixer in this case for consistency. + int need_master = 1; + if (!strcmp(config.mixer_channels[0], "Master") || !strcmp(config.mixer_channels[0], SJIS_MASTER)) { + need_master = 0; + } + nr_mixers = config.mixer_nr_channels + need_master; + mixers = xcalloc(nr_mixers, sizeof(struct mixer)); + for (unsigned i = 0; i < config.mixer_nr_channels; i++) { + mixers[i].name = config.mixer_channels[i]; + } + if (need_master) { + mixers[nr_mixers-1].name = "Master"; + master = &mixers[nr_mixers-1]; + } else { + master = &mixers[0]; + } + } + + // initialize mixer hierarchy + // NOTE: "Master" and "Voice" are special nodes; all channels following + // them in the list become their children. This appears to be + // hardcoded behavior in system 4. + struct mixer *parent = master; + for (int i = 0; i < nr_mixers; i++) { + if (&mixers[i] == master) + continue; + mixers[i].parent = parent; + parent->children = xrealloc_array(parent->children, parent->nr_children, parent->nr_children+1, sizeof(struct mixer*)); + parent->children[parent->nr_children++] = &mixers[i]; + if (!strcmp(mixers[i].name, "Voice") || !strcmp(mixers[i].name, SJIS_VOICE)) { + parent = &mixers[i]; + } + } + + // initialize mixers + for (int i = 0; i < nr_mixers; i++) { + sts_mixer_init(&mixers[i].mixer, 44100, STS_MIXER_SAMPLE_FORMAT_FLOAT); + int volume = i < (int)config.mixer_nr_channels ? config.mixer_volumes[i] : config.default_volume; + mixers[i].mixer.gain = clamp(0.0f, 1.0f, (float)volume / 100.0f); + } + + // initialize mixer streams + for (int i = 0; i < nr_mixers; i++) { + if (&mixers[i] == master) + continue; + mixers[i].stream.userdata = &mixers[i]; + mixers[i].stream.callback = refill_mixer; + mixers[i].stream.sample.frequency = 44100; + mixers[i].stream.sample.audio_format = STS_MIXER_SAMPLE_FORMAT_FLOAT; + mixers[i].stream.sample.length = CHUNK_SIZE * 2; + mixers[i].stream.sample.data = mixers[i].data; + mixers[i].voice = sts_mixer_play_stream(&mixers[i].parent->mixer, &mixers[i].stream, 1.0f); + } + + // read audio metadata + if (config.bgi_path) + bgi_read(config.bgi_path); + if (config.wai_path) + wai_load(config.wai_path); + + // initialize SDL audio SDL_AudioSpec have; SDL_AudioSpec want = { .format = AUDIO_F32, @@ -501,9 +617,53 @@ void mixer_init(void) .callback = audio_callback, }; audio_device = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); - sts_mixer_init(&mixer, 44100, STS_MIXER_SAMPLE_FORMAT_FLOAT); - if (config.bgi_path) - bgi_read(config.bgi_path); - SDL_PauseAudioDevice(audio_device, 0); } + +int mixer_get_numof(void) +{ + return nr_mixers; +} + +const char *mixer_get_name(int n) +{ + if (n < 0 || n >= nr_mixers) + return NULL; + return mixers[n].name; +} + +int mixer_get_volume(int n, int *volume) +{ + if (n < 0 || n >= nr_mixers) { + return 0; + } + SDL_LockAudioDevice(audio_device); + *volume = clamp(0, 100, (int)(mixers[n].mixer.gain * 100)); + SDL_UnlockAudioDevice(audio_device); + return 1; +} + +int mixer_set_volume(int n, int volume) +{ + if (n < 0 || n >= nr_mixers) + return 0; + SDL_LockAudioDevice(audio_device); + mixers[n].mixer.gain = clamp(0.0f, 1.0f, (float)volume / 100.0f); + SDL_UnlockAudioDevice(audio_device); + return 1; +} +int mixer_get_mute(int n, int *mute) +{ + if (n < 0 || n >= nr_mixers) + return 0; + *mute = mixers[n].muted; + return 1; +} + +int mixer_set_mute(int n, int mute) +{ + if (n < 0 || n >= nr_mixers) + return 0; + mixers[n].muted = !!mute; + return 1; +} diff --git a/src/hll/SystemService.c b/src/hll/SystemService.c index a4bcdb7..33e2396 100644 --- a/src/hll/SystemService.c +++ b/src/hll/SystemService.c @@ -18,56 +18,21 @@ #include "system4/string.h" +#include "mixer.h" #include "xsystem4.h" #include "hll.h" -static int SystemService_GetMixerNumof(void) -{ - return config.mixer_nr_channels; -} - static int SystemService_GetMixerName(int n, struct string **name) { - if (n < 0 || (size_t)n >= config.mixer_nr_channels) + const char *r = mixer_get_name(n); + if (!r) return 0; - *name = make_string(config.mixer_channels[n], strlen(config.mixer_channels[n])); + *name = make_string(r, strlen(r)); return 1; } -static int SystemService_GetMixerVolume(int n, int *volume) -{ - if (n < 0 || (size_t)n >= config.mixer_nr_channels) - return 0; - *volume = 100; - return 1; -} - -static int SystemService_GetMixerMute(int n, int *mute) -{ - if (n < 0 || (size_t)n >= config.mixer_nr_channels) - return 0; - *mute = 0; - return true; -} - //bool SystemService_SetMixerName(int nNum, string szName); -static int SystemService_SetMixerVolume(int n, possibly_unused int volume) -{ - if (n < 0 || (size_t)n >= config.mixer_nr_channels) - return 0; - // TODO - return 1; -} - -static int SystemService_SetMixerMute(int n, possibly_unused int mute) -{ - if (n < 0 || (size_t)n >= config.mixer_nr_channels) - return 0; - // TODO - return 1; -} - //int SystemService_GetGameVersion(void); static void SystemService_GetGameName(struct string **game_name) { @@ -163,13 +128,13 @@ static bool SystemService_GetMouseCursorConfig(int type, int *value) static void SystemService_RestrainScreensaver(void) { } HLL_LIBRARY(SystemService, - HLL_EXPORT(GetMixerNumof, SystemService_GetMixerNumof), + HLL_EXPORT(GetMixerNumof, mixer_get_numof), HLL_EXPORT(GetMixerName, SystemService_GetMixerName), - HLL_EXPORT(GetMixerVolume, SystemService_GetMixerVolume), - HLL_EXPORT(GetMixerMute, SystemService_GetMixerMute), + HLL_EXPORT(GetMixerVolume, mixer_get_volume), + HLL_EXPORT(GetMixerMute, mixer_get_mute), HLL_TODO_EXPORT(SetMixerName, SystemService_SetMixerName), - HLL_EXPORT(SetMixerVolume, SystemService_SetMixerVolume), - HLL_EXPORT(SetMixerMute, SystemService_SetMixerMute), + HLL_EXPORT(SetMixerVolume, mixer_set_volume), + HLL_EXPORT(SetMixerMute, mixer_set_mute), HLL_TODO_EXPORT(GetGameVersion, SystemService_GetGameVersion), HLL_EXPORT(GetGameName, SystemService_GetGameName), HLL_TODO_EXPORT(AddURLMenu, SystemService_AddURLMenu), diff --git a/src/system4.c b/src/system4.c index 76ae709..d81168c 100644 --- a/src/system4.c +++ b/src/system4.c @@ -51,6 +51,7 @@ struct config config = { .view_height = 600, .mixer_nr_channels = 0, .mixer_channels = NULL, + .default_volume = 100, .joypad = false, }; @@ -120,6 +121,7 @@ static void read_mixer_channels(struct ini_entry *entry) config.mixer_nr_channels = entry->value.list_size; config.mixer_channels = xcalloc(entry->value.list_size, sizeof(char*)); + config.mixer_volumes = xcalloc(entry->value.list_size, sizeof(int)); for (size_t i = 0; i < entry->value.list_size; i++) { if (entry->value.list[i].type == INI_NULL) { WARNING("No name given for VolumeValancer[%d]", i); @@ -127,6 +129,8 @@ static void read_mixer_channels(struct ini_entry *entry) } if (entry->value.list[i].type == INI_STRING) { config.mixer_channels[i] = strdup(entry->value.list[i].s->text); + // XXX: assumes default_volume was already initialized + config.mixer_volumes[i] = config.default_volume; } else if (entry->value.list[i].type == INI_LIST) { if (entry->value.list[i].list_size != 2) ERROR("ini value for 'VolumeValancer' is list of wrong size"); @@ -135,6 +139,7 @@ static void read_mixer_channels(struct ini_entry *entry) if (entry->value.list[i].list[1].type != INI_INTEGER) ERROR("ini value for 'VolumeValancer' level is not an integer"); config.mixer_channels[i] = strdup(entry->value.list[i].list[0].s->text); + config.mixer_volumes[i] = entry->value.list[i].list[1].i; } } } @@ -159,6 +164,8 @@ static void read_config(const char *path) config.view_height = ini_integer(&ini[i]); } else if (!strcmp(ini[i].name->text, "VolumeValancer")) { read_mixer_channels(&ini[i]); + } else if (!strcmp(ini[i].name->text, "DefaultVolumeRate")) { + config.default_volume = ini_integer(&ini[i]); } ini_free_entry(&ini[i]); }