From 68c2530b16cc213d5172f2f2c56688fd798df510 Mon Sep 17 00:00:00 2001 From: Sam Hocevar Date: Mon, 1 Apr 2019 23:23:43 +0200 Subject: [PATCH] audio: refactor audio streaming mechanism. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename “channels” to “tracks” when talking about the mixing channels, to avoid confusion with channels in the context of mono/stereo/5.1. Also allow conversions between various formats and frequencies. To do: allow to query the current audio device’s preferred frequency when the client application is able to send the best possible data. --- doc/tutorial/09_sound.cpp | 14 ++-- src/audio/audio.cpp | 166 ++++++++++++++++++++++++++++---------- src/lol/audio/audio.h | 27 +++++-- 3 files changed, 151 insertions(+), 56 deletions(-) diff --git a/doc/tutorial/09_sound.cpp b/doc/tutorial/09_sound.cpp index 602f5d93..15ea88b1 100644 --- a/doc/tutorial/09_sound.cpp +++ b/doc/tutorial/09_sound.cpp @@ -31,7 +31,7 @@ public: auto f = std::bind(&sound_demo::synth, this, i, std::placeholders::_1, std::placeholders::_2); - m_streams[i] = audio::start_streaming(f); + m_streams[i] = audio::start_streaming(f, audio::format::int16le, 22050, 1); } for (size_t i = 0; i < m_instrument.size(); ++i) @@ -54,20 +54,18 @@ public: int mode = (1 << channel) & m_mask; int16_t *stream = (int16_t *)buf; - for (int i = 0; i < bytes / 2; i += 2) + for (int i = 0; i < bytes / 2; ++i) { switch (mode) { - case 2: // square / triangle signals - stream[i] = 800 * (i % 128 > 64 ? -1 : 1); - stream[i + 1] = (i % 128 - 64) * 15; + case 2: // triangle signal + stream[i] = (i % 128 - 64) * 8; break; case 1: // white noise - stream[i] = lol::rand(-2000, 2000); - stream[i + 1] = lol::rand(-1000, 1000); + stream[i] = lol::rand(-2048, 2048); break; case 0: // inactive - stream[i] = stream[i + 1] = 0; + stream[i] = 0; break; } } diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp index 41baa9a1..f9d9cbf3 100644 --- a/src/audio/audio.cpp +++ b/src/audio/audio.cpp @@ -30,24 +30,75 @@ // “[…] refers to the size of the audio buffer in sample frames. A sample frame // is a chunk of audio data of the size specified in format multiplied by the // number of channels.” -#define LOL_AUDIO_SAMPLE_FRAMES 1024 -#define LOL_AUDIO_CHANNELS 2 +#define LOL_AUDIO_DEFAULT_FRAMES 1024 + +#define LOL_AUDIO_DEFAULT_TRACKS 8 +#define LOL_AUDIO_DEFAULT_CHANNELS 2 +#define LOL_AUDIO_DEFAULT_FORMAT AUDIO_S16 +#define LOL_AUDIO_DEFAULT_RATE 22050 namespace lol { struct audio_streamer { - int m_channel; + int m_track = -1; std::function m_callback; - std::array m_empty; // SDL needs a reference to this + // Buffer where the streaming client will write, and where the SDL + // audio conversion may happen. + std::vector m_convert_buffer; + // Remaining bytes in the conversion buffer. + int m_extra_bytes = 0; + // Buffer used to write the converted audio data for mixing. + std::vector m_output_buffer; #if defined LOL_USE_SDL_MIXER - Mix_Chunk *m_chunk; + SDL_AudioCVT m_convert; + Mix_Chunk *m_chunk = nullptr; #endif }; std::unordered_set> audio::m_streamers; +// The global audio format +static int g_frequency, g_channels; +static audio::format g_format; + +static audio::format sdl2lol_format(Uint16 sdl_format) +{ + switch (sdl_format) + { + case AUDIO_U8: return audio::format::uint8; + case AUDIO_S8: return audio::format::int8; + case AUDIO_U16LSB: return audio::format::uint16le; + case AUDIO_U16MSB: return audio::format::uint16be; + case AUDIO_S16LSB: return audio::format::int16le; + case AUDIO_S16MSB: return audio::format::int16be; + case AUDIO_S32LSB: return audio::format::int32le; + case AUDIO_S32MSB: return audio::format::int32be; + case AUDIO_F32LSB: return audio::format::float32le; + case AUDIO_F32MSB: return audio::format::float32be; + default: return audio::format::unknown; + } +} + +static int lol2sdl_format(audio::format format) +{ + switch (format) + { + case audio::format::uint8: return AUDIO_U8; + case audio::format::int8: return AUDIO_S8; + case audio::format::uint16le: return AUDIO_U16LSB; + case audio::format::uint16be: return AUDIO_U16MSB; + case audio::format::int16le: return AUDIO_S16LSB; + case audio::format::int16be: return AUDIO_S16MSB; + case audio::format::int32le: return AUDIO_S32LSB; + case audio::format::int32be: return AUDIO_S32MSB; + case audio::format::float32le: return AUDIO_F32LSB; + case audio::format::float32be: return AUDIO_F32MSB; + default: return 0; + } +} + /* * Public audio class */ @@ -55,48 +106,40 @@ std::unordered_set> audio::m_streamers; #if defined LOL_USE_SDL_MIXER void audio::init() { - if (Mix_OpenAudio(22050, AUDIO_S16, LOL_AUDIO_CHANNELS, LOL_AUDIO_SAMPLE_FRAMES) < 0) + if (Mix_OpenAudio(LOL_AUDIO_DEFAULT_RATE, LOL_AUDIO_DEFAULT_FORMAT, LOL_AUDIO_DEFAULT_CHANNELS, LOL_AUDIO_DEFAULT_FRAMES) < 0) { msg::error("error opening audio: %s\n", Mix_GetError()); return; } - set_channels(8); + set_tracks(LOL_AUDIO_DEFAULT_TRACKS); - int frequency, channels; - Uint16 format; - if (Mix_QuerySpec(&frequency, &format, &channels) == 0) + Uint16 sdl_format; + if (Mix_QuerySpec(&g_frequency, &sdl_format, &g_channels) == 0) { msg::error("error querying audio: %s\n", Mix_GetError()); return; } - char const *sformat = "unknown"; - switch (format) - { - case AUDIO_U8: sformat = "uint8"; break; - case AUDIO_S8: sformat = "int8"; break; - case AUDIO_U16LSB: sformat = "uint16le"; break; - case AUDIO_U16MSB: sformat = "uint16be"; break; - case AUDIO_S16LSB: sformat = "int16le"; break; - case AUDIO_S16MSB: sformat = "int16be"; break; - case AUDIO_S32LSB: sformat = "int32le"; break; - case AUDIO_S32MSB: sformat = "int32be"; break; - case AUDIO_F32LSB: sformat = "float32le"; break; - case AUDIO_F32MSB: sformat = "float32be"; break; - } - msg::info("audio freq=%dHz format=%s channels=%d\n", - frequency, sformat, channels); + g_format = sdl2lol_format(sdl_format); + + char const *u = (SDL_AUDIO_ISFLOAT(sdl_format) || SDL_AUDIO_ISSIGNED(sdl_format)) ? "" : "u"; + char const *t = SDL_AUDIO_ISFLOAT(sdl_format) ? "float" : "int"; + int b = SDL_AUDIO_BITSIZE(sdl_format); + char const *e = b <= 8 ? "" : SDL_AUDIO_ISLITTLEENDIAN(sdl_format) ? "le" : "be"; + + msg::info("audio initialised: freq=%dHz format=%s%s%d%s channels=%d\n", + g_frequency, u, t, b, e, g_channels); } -void audio::set_channels(int channels) +void audio::set_tracks(int tracks) { - Mix_AllocateChannels(channels); + Mix_AllocateChannels(tracks); } -void audio::set_volume(int channel, int volume) +void audio::set_volume(int track, int volume) { - Mix_Volume(channel, volume); + Mix_Volume(track, volume); } void audio::mute_all() @@ -109,35 +152,72 @@ void audio::unmute_all() Mix_Volume(-1, MIX_MAX_VOLUME); } -int audio::start_streaming(std::function const &f) +int audio::start_streaming(std::function const &f, + enum audio::format format /* = audio::format::uint16le */, + int frequency /* = 22050 */, + int channels /* = 2 */) { static auto trampoline = [](int, void *stream, int bytes, void *udata) { auto s = (audio_streamer *)udata; - s->m_callback(stream, bytes); + + // If there were still bytes from a previous conversion, copy them. + if (s->m_extra_bytes) + { + int tocopy = lol::min(bytes, s->m_extra_bytes); + memcpy(stream, s->m_convert.buf + s->m_convert.len_cvt - s->m_extra_bytes, tocopy); + s->m_extra_bytes -= tocopy; + bytes -= tocopy; + stream = (void *)((uint8_t *)stream + tocopy); + } + + // Ask the callback for more bytes as long as we need them. + while (bytes > 0) + { + s->m_callback(s->m_convert.buf, s->m_convert.len); + if (s->m_convert.needed) + SDL_ConvertAudio(&s->m_convert); + int tocopy = lol::min(bytes, s->m_convert.len_cvt); + s->m_extra_bytes = s->m_convert.len_cvt - tocopy; + memcpy(stream, s->m_convert.buf, tocopy); + bytes -= tocopy; + stream = (void *)((uint8_t *)stream + tocopy); + } }; auto s = std::make_shared(); m_streamers.insert(s); - Uint8* audio_data = (Uint8*)s->m_empty.data(); - Uint32 audio_size = (Uint32)(s->m_empty.size() * sizeof(s->m_empty[0])); - memset(audio_data, 17, audio_size); - s->m_chunk = Mix_QuickLoad_RAW(audio_data, audio_size); - s->m_channel = Mix_PlayChannel(-1, s->m_chunk, -1); + // Build an audio converter that converts from a given streaming format + // to the format SDL was currently initialised with, if necessary. + Uint16 sdl_format = lol2sdl_format(format); + Uint16 sdl_g_format = lol2sdl_format(g_format); + SDL_BuildAudioCVT(&s->m_convert, sdl_format, channels, frequency, sdl_g_format, g_channels, g_frequency); + + // This is how many bytes we will ask the streaming callback + s->m_convert.len = LOL_AUDIO_DEFAULT_FRAMES * channels * SDL_AUDIO_BITSIZE(sdl_format) / 8; + s->m_convert_buffer.resize(s->m_convert.len * s->m_convert.len_mult); + s->m_convert.buf = s->m_convert_buffer.data(); + + // This is how many bytes we will send to the SDL mixer + Uint32 output_bytes = LOL_AUDIO_DEFAULT_FRAMES * g_channels * SDL_AUDIO_BITSIZE(sdl_g_format) / 8; + s->m_output_buffer.resize(output_bytes); + Uint8* audio_data = (Uint8*)s->m_output_buffer.data(); + s->m_chunk = Mix_QuickLoad_RAW(audio_data, output_bytes); + s->m_track = Mix_PlayChannel(-1, s->m_chunk, -1); s->m_callback = f; - Mix_RegisterEffect(s->m_channel, trampoline, nullptr, s.get()); + Mix_RegisterEffect(s->m_track, trampoline, nullptr, s.get()); - return s->m_channel; + return s->m_track; } -void audio::stop_streaming(int stream) +void audio::stop_streaming(int track) { for (auto streamer : m_streamers) { - if (streamer->m_channel == stream) + if (streamer->m_track == track) { - Mix_HaltChannel(stream); + Mix_HaltChannel(track); m_streamers.erase(streamer); break; } @@ -146,7 +226,7 @@ void audio::stop_streaming(int stream) #else void audio::init() {} -void audio::set_channels(int) {} +void audio::set_tracks(int) {} void audio::set_volume(int, int) {} void audio::mute_all() {} void audio::unmute_all() {} diff --git a/src/lol/audio/audio.h b/src/lol/audio/audio.h index 33a0f9be..a82e3a9b 100644 --- a/src/lol/audio/audio.h +++ b/src/lol/audio/audio.h @@ -1,7 +1,7 @@ // // Lol Engine // -// Copyright © 2010—2016 Sam Hocevar +// Copyright © 2010—2019 Sam Hocevar // // Lol Engine is free software. It comes without any warranty, to // the extent permitted by applicable law. You can redistribute it @@ -27,15 +27,32 @@ namespace lol class audio { public: + enum class format : uint8_t + { + unknown = 0, + uint8, + int8, + uint16le, uint16be, + int16le, int16be, + int32le, int32be, + float32le, float32be, + }; + static void init(); - static void set_channels(int channels); - static void set_volume(int channel,int volume); + // Set the number of audio tracks that can be mixed together + static void set_tracks(int tracks); + // Set the volume of a specific track + static void set_volume(int track, int volume); static void mute_all(); static void unmute_all(); - static int start_streaming(std::function const &f); - static void stop_streaming(int channel); + static int start_streaming(std::function const &f, + format format = audio::format::uint16le, + int frequency = 22050, + int channels = 2); + + static void stop_streaming(int track); private: audio() {}