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]); }