#include "twn_audio_c.h" #include "twn_engine_context_c.h" #include "twn_util.h" #include "twn_util_c.h" #include "twn_audio.h" #include #include #include #include #define STB_VORBIS_NO_PUSHDATA_API #define STB_VORBIS_HEADER_ONLY #include #include #include static const char *audio_exts[AUDIO_FILE_TYPE_COUNT] = { ".ogg", /* AUDIO_FILE_TYPE_OGG */ ".wav", /* AUDIO_FILE_TYPE_WAV */ ".xm", /* AUDIO_FILE_TYPE_XM */ }; static const uint8_t audio_exts_len[AUDIO_FILE_TYPE_COUNT] = { sizeof ".ogg" - 1, sizeof ".wav" - 1, sizeof ".xm" - 1, }; /* TODO: allow for vectorization and packed vectors (alignment care and alike) */ /* TODO: free channel name duplicates */ /* TODO: count frames without use, free the memory when threshold is met */ /* TODO: count repeated usages for sound effect cases with rendering to ram? */ /* stores path to data hash, useful for sound effects */ static struct AudioFileCache { char *key; struct AudioFileCacheValue { unsigned char *data; size_t len; } value; } *audio_file_cache; static int64_t get_audio_data(const char *path, unsigned char **data) { const struct AudioFileCache *cache = shgetp_null(audio_file_cache, path); if (!cache) { unsigned char *file; int64_t len = file_to_bytes(path, &file); if (len == -1) { CRY("Audio error", "Error reading file"); return -1; } const struct AudioFileCacheValue value = { file, (size_t)len }; shput(audio_file_cache, path, value); *data = file; return len; } *data = cache->value.data; return (int64_t)cache->value.len; } static AudioFileType infer_audio_file_type(const char *path) { size_t const path_len = SDL_strlen(path); for (int i = 0; i < AUDIO_FILE_TYPE_COUNT; ++i) { if (SDL_strncmp(&path[path_len - audio_exts_len[i]], audio_exts[i], audio_exts_len[i]) == 0) return (AudioFileType)i; } return AUDIO_FILE_TYPE_UNKNOWN; } /* TODO: error propagation and clearing of resources on partial success? */ /* or should we expect things to simply fail? */ /* TODO: reuse often used decoded/decompressed data */ static union AudioContext init_audio_context(const char *path, AudioFileType type) { switch (type) { case AUDIO_FILE_TYPE_OGG: { unsigned char *data; int64_t len = get_audio_data(path, &data); if (len == -1) { CRY("Audio error", "Error reading file"); break; } int error = 0; stb_vorbis* handle = stb_vorbis_open_memory(data, (int)len, &error, NULL); if (error != 0) { CRY("Audio error", "Error reading .ogg file"); break; } stb_vorbis_info info = stb_vorbis_get_info(handle); return (union AudioContext) { .vorbis = { .data = data, .handle = handle, .frequency = info.sample_rate, .channel_count = (uint8_t)info.channels, } }; } /* TODO: transform to destination format immediately? */ case AUDIO_FILE_TYPE_WAV: { SDL_AudioSpec spec; uint8_t *data; uint32_t len; if (!SDL_LoadWAV_RW(PHYSFSRWOPS_openRead(path), 1, &spec, &data, &len)) { CRY_SDL("Cannot load .wav file:"); break; } SDL_AudioCVT cvt; int conv = SDL_BuildAudioCVT(&cvt, spec.format, spec.channels, spec.freq, AUDIO_F32, 2, AUDIO_FREQUENCY); if (conv < 0) { CRY_SDL("Cannot resample .wav:"); break; } if (conv != 0) { data = SDL_realloc(data, len * cvt.len_mult); cvt.buf = data; cvt.len = len; if (SDL_ConvertAudio(&cvt) < 0) { CRY_SDL("Error resampling .wav:"); break; } spec.channels = 2; spec.freq = AUDIO_FREQUENCY; /* TODO: test this */ spec.samples = (uint16_t)((size_t)(SDL_floor((double)len * cvt.len_ratio)) / sizeof (float) / 2); } else { spec.samples = (uint16_t)((size_t)(SDL_floor((double)len * cvt.len_ratio)) / sizeof (float) / 2); } return (union AudioContext) { .wav = { .position = 0, .samples = data, .spec = spec, } }; } case AUDIO_FILE_TYPE_XM: { unsigned char *data; int64_t len = get_audio_data(path, &data); if (len == -1) { CRY("Audio error", "Error reading file"); break; } xm_context_t *handle; int response = xm_create_context_safe(&handle, (const char *)data, (size_t)len, AUDIO_FREQUENCY); if (response != 0) { CRY("Audio error", "Error loading xm module"); break; } xm_set_max_loop_count(handle, 1); return (union AudioContext) { .xm = { .handle = handle } }; } case AUDIO_FILE_TYPE_UNKNOWN: case AUDIO_FILE_TYPE_COUNT: default: CRY("Audio error", "Unhandled audio format (in init)"); return (union AudioContext){0}; } return (union AudioContext){0}; } static void free_audio_channel(AudioChannel channel) { switch (channel.file_type) { case AUDIO_FILE_TYPE_OGG: { SDL_free(channel.context.vorbis.data); break; } case AUDIO_FILE_TYPE_WAV: { SDL_free(channel.context.wav.samples); break; } case AUDIO_FILE_TYPE_XM: { xm_free_context(channel.context.xm.handle); break; } case AUDIO_FILE_TYPE_COUNT: case AUDIO_FILE_TYPE_UNKNOWN: default: SDL_assert_always(false); break; } SDL_free(channel.path); } static void repeat_audio(AudioChannel *channel) { switch (channel->file_type) { case AUDIO_FILE_TYPE_OGG: { stb_vorbis_seek_start(channel->context.vorbis.handle); break; } case AUDIO_FILE_TYPE_WAV: { channel->context.wav.position = 0; break; } case AUDIO_FILE_TYPE_XM: { xm_restart(channel->context.xm.handle); break; } case AUDIO_FILE_TYPE_UNKNOWN: case AUDIO_FILE_TYPE_COUNT: default: CRY("Audio error", "Unhandled audio format (in repeat)"); break; } } void audio_play(const char *path, const char *channel, bool repeat, float volume, float panning) { if (!ctx.audio_initialized) { profile_start("audio initialization"); SDL_AudioSpec request, got; SDL_zero(request); request.freq = AUDIO_FREQUENCY; request.format = AUDIO_F32; request.channels = 2; request.samples = 4096; if (!ctx.push_audio_model) request.callback = audio_callback; /* TODO: check for errors */ ctx.audio_device = SDL_OpenAudioDevice(NULL, 0, &request, &got, 0); ctx.audio_stream_format = got.format; ctx.audio_stream_frequency = got.freq; ctx.audio_stream_channel_count = got.channels; /* TODO: relax this */ SDL_assert_always(got.freq == AUDIO_FREQUENCY); SDL_assert_always(got.format == AUDIO_F32); SDL_assert_always(got.channels == 2); SDL_PauseAudioDevice(ctx.audio_device, 0); profile_end("audio initialization"); ctx.audio_initialized = true; } SDL_LockAudioDevice(ctx.audio_device); if (channel) { AudioChannelItem *pair = shgetp_null(ctx.audio_channels, channel); /* create a channel if it doesn't exist */ if (!pair) { AudioFileType const file_type = infer_audio_file_type(path); AudioChannel const new_channel = { .file_type = file_type, .context = init_audio_context(path, file_type), .path = SDL_strdup(path), .repeat = repeat, .volume = volume, .panning = panning, }; shput(ctx.audio_channels, SDL_strdup(channel), new_channel); pair = shgetp_null(ctx.audio_channels, channel); } /* repeat if so */ else if (strcmp(pair->value.path, path) == 0) repeat_audio(&pair->value); else { /* start anew, reuse channel name */ free_audio_channel(pair->value); AudioFileType const file_type = infer_audio_file_type(path); AudioChannel const new_channel = { .file_type = file_type, .context = init_audio_context(path, file_type), .path = SDL_strdup(path), .repeat = repeat, .volume = volume, .panning = panning, }; pair->value = new_channel; } } else { /* audio without channel plays without repeat and ability to change parameters over its course, nor stop it */ AudioFileType const file_type = infer_audio_file_type(path); AudioChannel new_channel = { .file_type = file_type, .context = init_audio_context(path, file_type), .path = SDL_strdup(path), .repeat = false, .volume = volume, .panning = panning, }; if (repeat) log_warn("Cannot repeat audio played on unnamed scratch channel (for %s)", path); arrpush(ctx.unnamed_audio_channels, new_channel); } SDL_UnlockAudioDevice(ctx.audio_device); } TWN_API void audio_parameter(const char *channel, const char *param, float value) { AudioChannelItem *pair = shgetp_null(ctx.audio_channels, channel); if (!pair) { log_warn("No channel by the name of %s to set a parameter for", channel); return; } if (SDL_strncmp(param, "repeat", sizeof "repeat" - 1) == 0) { pair->value.repeat = (bool)value; } else if (SDL_strncmp(param, "volume", sizeof "volume" - 1) == 0) { if (value > 1.0f || value < 0.0f) { log_warn("Out of range volume for channel %s set", channel); value = clampf(value, 0.0f, 1.0f); } pair->value.volume = value; } else if (SDL_strncmp(param, "panning", sizeof "panning" - 1) == 0) { if (value > 1.0f || value < -1.0f) { log_warn("Out of range panning for channel %s set", channel); value = clampf(value, -1.0f, +1.0f); } pair->value.panning = value; } else CRY("Audio channel parameter setting failed", "Invalid parameter enum given"); } /* TODO: handle it more properly in regards to clipping and alike */ /* this assumes float based streams */ static void audio_mixin_streams(const AudioChannel *channel, uint8_t *restrict a, uint8_t *restrict b, size_t frames, bool last) { float *const sa = (float *)(void *)a; float *const sb = (float *)(void *)b; const float left_panning = fminf(fabsf(channel->panning - 1.0f), 1.0f); const float right_panning = fminf(fabsf(channel->panning + 1.0f), 1.0f); if (last) { for (size_t s = 0; s < frames; ++s) { /* left channel */ sa[s * 2 + 0] += (float)(sb[s * 2 + 0] * channel->volume * left_panning); /* right channel */ sa[s * 2 + 1] += (float)(sb[s * 2 + 1] * channel->volume * right_panning); } } else { for (size_t s = 0; s < frames; ++s) { /* left channel */ sa[s * 2 + 0] += (float)(sb[s * 2 + 0] * channel->volume * left_panning); sa[s * 2 + 0] *= 1 / (float)M_2_SQRTPI; /* right channel */ sa[s * 2 + 1] += (float)(sb[s * 2 + 1] * channel->volume * right_panning); sa[s * 2 + 1] *= 1 / (float)M_2_SQRTPI; } } } /* remember: frame consists of sample * channel_count */ static void audio_sample_and_mixin_channel(AudioChannel *channel, uint8_t *stream, int len, bool last) { static uint8_t buffer[16384]; /* TODO: better make it a growable scratch instead, which will simplify things */ const size_t float_buffer_frames = sizeof (buffer) / sizeof (float) / 2; const size_t stream_frames = len / sizeof (float) / 2; switch (channel->file_type) { case AUDIO_FILE_TYPE_OGG: { /* feed stream for needed conversions */ for (size_t i = 0; i < stream_frames; ) { const size_t n_frames = (stream_frames - i) > float_buffer_frames ? float_buffer_frames : stream_frames - i; const size_t samples_per_channel = stb_vorbis_get_samples_float_interleaved( channel->context.vorbis.handle, 2, (float *)buffer, (int)n_frames * 2); /* handle end of file */ if (samples_per_channel == 0) { if (channel->repeat) { /* seek to start and try sampling some more */ stb_vorbis_seek_start(channel->context.vorbis.handle); continue; } else { /* leave silence */ channel->finished = true; break; } } /* panning and mixing */ audio_mixin_streams(channel, &stream[i * sizeof(float) * 2], buffer, samples_per_channel, last); i += samples_per_channel; } break; } case AUDIO_FILE_TYPE_WAV: { /* feed stream for needed conversions */ for (size_t i = 0; i < stream_frames; ) { const size_t limit = MIN(stream_frames - i, channel->context.wav.spec.samples - channel->context.wav.position); /* same format, just feed it directly */ audio_mixin_streams(channel, &stream[i * sizeof(float) * 2], &((uint8_t *)channel->context.wav.samples)[channel->context.wav.position * sizeof (float) * 2], limit, last); channel->context.wav.position += limit; if (channel->context.wav.position >= channel->context.wav.spec.samples) { if (channel->repeat) channel->context.wav.position = 0; else { /* leave silence */ channel->finished = true; break; } } i += limit; } break; } case AUDIO_FILE_TYPE_XM: { for (size_t i = 0; i < stream_frames; ) { const size_t n_frames = (stream_frames - i) > float_buffer_frames ? float_buffer_frames : stream_frames - i; const size_t samples_per_channel = xm_generate_samples(channel->context.xm.handle, (float *)buffer, n_frames); /* handle end of file */ if (samples_per_channel == 0) { if (channel->repeat) { /* seek to start and try sampling some more */ xm_restart(channel->context.xm.handle); continue; } else { channel->finished = true; /* leave silence */ break; } } /* panning and mixing */ audio_mixin_streams(channel, &stream[i * sizeof(float) * 2], buffer, samples_per_channel, last); i += samples_per_channel; } break; } case AUDIO_FILE_TYPE_UNKNOWN: case AUDIO_FILE_TYPE_COUNT: default: CRY("Audio error", "Unhandled audio format (in sampling)"); break; } } static void sanity_check_channel(const AudioChannelItem channel) { if (channel.value.volume < 0.0f || channel.value.volume > 1.0f) log_warn("Volume argument is out of range for channel (%s)", channel.key ? channel.key : "unnamed"); if (channel.value.panning < -1.0f || channel.value.panning > 1.0f) log_warn("Panning argument is out of range for channel (%s)", channel.key ? channel.key : "unnamed"); } void audio_callback(void *userdata, uint8_t *stream, int len) { (void)userdata; /* prepare for mixing */ SDL_memset(stream, 0, len); size_t const audio_channels_len = shlen(ctx.audio_channels); for (size_t i = 0; i < audio_channels_len; ++i) { sanity_check_channel(ctx.audio_channels[i]); audio_sample_and_mixin_channel(&ctx.audio_channels[i].value, stream, len, i == audio_channels_len - 1); } size_t const unnamed_audio_channels_len = shlen(ctx.unnamed_audio_channels); for (size_t i = 0; i < unnamed_audio_channels_len; ++i) { sanity_check_channel((AudioChannelItem){NULL, ctx.unnamed_audio_channels[i]}); audio_sample_and_mixin_channel(&ctx.unnamed_audio_channels[i], stream, len, i == unnamed_audio_channels_len - 1); } /* ditch finished unnamed */ size_t i = 0; while (i < unnamed_audio_channels_len) { if (ctx.unnamed_audio_channels[i].finished) { free_audio_channel(ctx.unnamed_audio_channels[i]); arrdelswap(ctx.unnamed_audio_channels, i); } else i++; } } TWN_API void audio_play_args(PlayAudioArgs args) { const char *channel = m_or(args, channel, NULL); const bool repeat = m_or(args, repeat, false); const float volume = m_or(args, volume, 1.0f); const float panning = m_or(args, panning, 0.0f); audio_play(args.path, channel, repeat, volume, panning); }