/////////////////////////////////////////////////////////////////////////////// // sts_mixer.h - v0.02 // written 2016 by Sebastian Steinhauer // // LICENSE // Public domain. See "unlicense" statement at the end of this file. // // ABOUT // A simple stereo audio mixer which is capable of mixing samples and audio streams. // Samples can be played with different gain, pitch and panning. // Streams can be played with different gain. // This library has no malloc/free. All structs have to be "prepared" by the user. So you can enroll your own memory management. // You have to implement/provide a real audio-backend to hear something from the speakers. // A good starting point would be SDL2 where you can use an audio callback to feed the audio device. // // USAGE // Please note that most audio systems will run in a separate thread. So you have to take care about locking before modifying the sts_mixer_t state. // See the example at the end of the file. // // VERSION HISTORY // 0.02 (2022-05-10) allow voice queueing in same channel. ie, chain another sample on same voice channel after current sample playback is done (@r-lyeh) // 0.01 (2016-05-01) initial version // #ifndef __INCLUDED__STS_MIXER_H__ #define __INCLUDED__STS_MIXER_H__ // The number of concurrent voices (channels) which are used to mix the audio. // If you need more, use a higher number by setting #define STS_MIXER_VOICE n before including this header. #ifndef STS_MIXER_VOICES #define STS_MIXER_VOICES 32 #endif // STS_MIXER_VOICES // Defines the various audio formats. Note that they are all on system endianess. enum { STS_MIXER_SAMPLE_FORMAT_NONE, // no format STS_MIXER_SAMPLE_FORMAT_8, // signed 8-bit STS_MIXER_SAMPLE_FORMAT_16, // signed 16-bit STS_MIXER_SAMPLE_FORMAT_32, // signed 32-bit STS_MIXER_SAMPLE_FORMAT_FLOAT // floats }; //////////////////////////////////////////////////////////////////////////////// // // SAMPLES // // A sample is a *MONO* piece of audio which is loaded fully to memory. // It can be played with various gains, pitches and pannings. // typedef struct { unsigned int length; // length in samples (so 1024 samples of STS_MIXER_SAMPLE_FORMAT_16 would be 2048 bytes) unsigned int frequency; // frequency of this sample (e.g. 44100, 22000 ...) int audio_format; // one of STS_MIXER_SAMPLE_FORMAT_* void* data; // pointer to the sample data, sts_mixer makes no copy, so you have to keep them in memory void* next; // next sample in chain (if any) //< @r-lyeh } sts_mixer_sample_t; //////////////////////////////////////////////////////////////////////////////// // // STREAMS // // A stream is *STEREO* audio which will be decoded/loaded as needed. // It can be played with various gains. No panning or pitching. // // The callback which will be called when the stream needs more data. typedef void (*sts_mixer_stream_callback)(sts_mixer_sample_t* sample, void* userdata); typedef struct { void* userdata; // a userdata pointer which will passed to the callback sts_mixer_stream_callback callback; // this callback will be called when the stream needs more data sts_mixer_sample_t sample; // the current stream "sample" which holds the current piece of audio } sts_mixer_stream_t; //////////////////////////////////////////////////////////////////////////////// // // VOICES // // A voice is an audio source which will be used during mixing. // It can play nothing, a sample or a stream. // Most of those fields are considered "private" and you should not play around with those. // typedef struct { int state; sts_mixer_sample_t* sample; sts_mixer_stream_t* stream; float position; float gain; float pitch; float pan; } sts_mixer_voice_t; //////////////////////////////////////////////////////////////////////////////// // // MIXER // // The mixer state. // typedef struct { float gain; // the global gain (you can change it if you want to change to overall volume) unsigned int frequency; // the frequency for the output of mixed audio data int audio_format; // the audio format for the output of mixed audio data sts_mixer_voice_t voices[STS_MIXER_VOICES]; // holding all audio voices for this state } sts_mixer_t; //////////////////////////////////////////////////////////////////////////////// // // API // // "Initializes" a new sts_mixer state. void sts_mixer_init(sts_mixer_t* mixer, unsigned int frequency, int audio_format); // "Shutdown" the mixer state. It will simply reset all fields. void sts_mixer_shutdown(sts_mixer_t* mixer); // Return the number of active voices. Active voices are voices that play either a stream or a sample. int sts_mixer_get_active_voices(sts_mixer_t* mixer); // Play the given sample with the gain, pitch and panning. // Panning can be something between -1.0f (fully left) ... +1.0f (fully right) // Please note that pitch will be clamped so it cannot reach 0.0f (would be useless). // Returns the number of the voice where this sample will be played or -1 if no voice was free. int sts_mixer_play_sample(sts_mixer_t* mixer, sts_mixer_sample_t* sample, float gain, float pitch, float pan); // Plays the given stream with the gain. // Returns the number of the voice where this stream will be played or -1 if no voice was free. int sts_mixer_play_stream(sts_mixer_t* mixer, sts_mixer_stream_t* stream, float gain); // Stops voice with the given voice no. You can pass the returned number of sts_mixer_play_sample / sts_mixer_play_stream here. void sts_mixer_stop_voice(sts_mixer_t* mixer, int voice); // Stops all voices playing the given sample. Useful when you want to delete the sample and make sure it is not used anymore. void sts_mixer_stop_sample(sts_mixer_t* mixer, sts_mixer_sample_t* sample); // Stops all voices playing the given stream. Useful when you want to delete the stream and make sure it is not used anymore. void sts_mixer_stop_stream(sts_mixer_t* mixer, sts_mixer_stream_t* stream); // The mixing function. You should call the function if you need to pass more audio data to the audio device. // Typically this function is called in a separate thread or something like that. // It will write audio data in the specified format and frequency of the mixer state. void sts_mixer_mix_audio(sts_mixer_t* mixer, void* output, unsigned int samples); #endif // __INCLUDED__STS_MIXER_H__ /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// //// //// IMPLEMENTATION //// //// #ifdef STS_MIXER_IMPLEMENTATION enum { STS_MIXER_VOICE_STOPPED, STS_MIXER_VOICE_PLAYING, STS_MIXER_VOICE_STREAMING }; static float sts_mixer__clamp(const float value, const float min, const float max) { if (value < min) return min; else if (value > max) return max; else return value; } static float sts_mixer__clamp_sample(const float sample) { if (sample < -1.0f) return -1.0f; else if (sample > 1.0f) return 1.0f; else return sample; } static float sts_mixer__get_sample(sts_mixer_sample_t* sample, unsigned int position) { switch (sample->audio_format) { case STS_MIXER_SAMPLE_FORMAT_8: return (float)((char*)sample->data)[position] / 127.0f; case STS_MIXER_SAMPLE_FORMAT_16: return (float)((short*)sample->data)[position] / 32767.0f; case STS_MIXER_SAMPLE_FORMAT_32: return (float)((int*)sample->data)[position] / 2147483647.0f; case STS_MIXER_SAMPLE_FORMAT_FLOAT: return ((float*)sample->data)[position]; default: return 0.0f; } } static void sts_mixer__reset_voice(sts_mixer_t* mixer, const int i) { sts_mixer_voice_t* voice = &mixer->voices[i]; voice->state = STS_MIXER_VOICE_STOPPED; voice->sample = 0; voice->stream = 0; voice->position = voice->gain = voice->pitch = voice->pan = 0.0f; } static int sts_mixer__find_free_voice(sts_mixer_t* mixer) { int i; for (i = 0; i < STS_MIXER_VOICES; ++i) { if (mixer->voices[i].state == STS_MIXER_VOICE_STOPPED) return i; } return -1; } void sts_mixer_init(sts_mixer_t* mixer, unsigned int frequency, int audio_format) { int i; for (i = 0; i < STS_MIXER_VOICES; ++i) sts_mixer__reset_voice(mixer, i); mixer->frequency = frequency; mixer->gain = 1.0f; mixer->audio_format = audio_format; } void sts_mixer_shutdown(sts_mixer_t* mixer) { sts_mixer_init(mixer, 0, 0); } int sts_mixer_get_active_voices(sts_mixer_t* mixer) { int i, active; for (i = 0, active = 0; i < STS_MIXER_VOICES; ++i) { if (mixer->voices[i].state != STS_MIXER_VOICE_STOPPED) ++active; } return active; } int sts_mixer_play_sample(sts_mixer_t* mixer, sts_mixer_sample_t* sample, float gain, float pitch, float pan) { int i; sts_mixer_voice_t* voice; i = sts_mixer__find_free_voice(mixer); if (i >= 0) { voice = &mixer->voices[i]; voice->gain = gain; voice->pitch = sts_mixer__clamp(pitch, 0.1f, 10.0f); voice->pan = sts_mixer__clamp(pan * 0.5f, -0.5f, 0.5f); voice->position = 0.0f; voice->sample = sample; voice->stream = 0; voice->state = STS_MIXER_VOICE_PLAYING; } return i; } int sts_mixer_play_stream(sts_mixer_t* mixer, sts_mixer_stream_t* stream, float gain) { int i; sts_mixer_voice_t* voice; i = sts_mixer__find_free_voice(mixer); if (i >= 0) { voice = &mixer->voices[i]; voice->gain = gain; voice->position = 0.0f; voice->sample = 0; voice->stream = stream; voice->state = STS_MIXER_VOICE_STREAMING; } return i; } void sts_mixer_stop_voice(sts_mixer_t* mixer, int voice) { if (voice >= 0 && voice < STS_MIXER_VOICES) sts_mixer__reset_voice(mixer, voice); } void sts_mixer_stop_sample(sts_mixer_t* mixer, sts_mixer_sample_t* sample) { int i; for (i = 0; i < STS_MIXER_VOICES; ++i) { if (mixer->voices[i].sample == sample) sts_mixer__reset_voice(mixer, i); } } void sts_mixer_stop_stream(sts_mixer_t* mixer, sts_mixer_stream_t* stream) { int i; for (i = 0; i < STS_MIXER_VOICES; ++i) { if (mixer->voices[i].stream == stream) sts_mixer__reset_voice(mixer, i); } } void sts_mixer_mix_audio(sts_mixer_t* mixer, void* output, unsigned int samples) { sts_mixer_voice_t* voice; unsigned int i, position; float left, right, advance, sample; char* out_8 = (char*)output; short* out_16 = (short*)output; int* out_32 = (int*)output; float* out_float = (float*)output; // mix all voices advance = 1.0f / (float)mixer->frequency; for (; samples > 0; --samples) { left = right = 0.0f; for (i = 0; i < STS_MIXER_VOICES; ++i) { voice = &mixer->voices[i]; if (voice->state == STS_MIXER_VOICE_PLAYING) { position = (int)voice->position; if (position < voice->sample->length) { sample = sts_mixer__clamp_sample(sts_mixer__get_sample(voice->sample, position) * voice->gain); left += sts_mixer__clamp_sample(sample * (0.5f - voice->pan)); right += sts_mixer__clamp_sample(sample * (0.5f + voice->pan)); voice->position += (float)voice->sample->frequency * advance * voice->pitch; } else if( voice->sample->next ) { //< @r-lyeh *voice->sample = *(sts_mixer_sample_t*)voice->sample->next; //< @r-lyeh voice->position = 0; //< @r-lyeh } else sts_mixer__reset_voice(mixer, i); } else if (voice->state == STS_MIXER_VOICE_STREAMING) { position = ((int)voice->position) * 2; if (position >= voice->stream->sample.length) { // buffer empty...refill voice->stream->callback(&voice->stream->sample, voice->stream->userdata); voice->position = 0.0f; position = 0; } left += sts_mixer__clamp_sample(sts_mixer__get_sample(&voice->stream->sample, position) * voice->gain); right += sts_mixer__clamp_sample(sts_mixer__get_sample(&voice->stream->sample, position + 1) * voice->gain); voice->position += (float)voice->stream->sample.frequency * advance; } } // write to buffer. float _g = mixer->gain; //< @r-lyeh: added master gain float _127 = 127.0f * _g; //< @r-lyeh: added master gain float _32767 = 32767.0f * _g; //< @r-lyeh: added master gain float _2147483647 = 2147483647.0f * _g; //< @r-lyeh: added master gain left = sts_mixer__clamp_sample(left); right = sts_mixer__clamp_sample(right); switch (mixer->audio_format) { case STS_MIXER_SAMPLE_FORMAT_8: *out_8++ = (char)(left * _127); //< @r-lyeh: added master gain *out_8++ = (char)(right * _127); //< @r-lyeh: added master gain break; case STS_MIXER_SAMPLE_FORMAT_16: *out_16++ = (short)(left * _32767); //< @r-lyeh: added master gain *out_16++ = (short)(right * _32767); //< @r-lyeh: added master gain break; case STS_MIXER_SAMPLE_FORMAT_32: *out_32++ = (int)(left * _2147483647); //< @r-lyeh: added master gain *out_32++ = (int)(right * _2147483647); //< @r-lyeh: added master gain break; case STS_MIXER_SAMPLE_FORMAT_FLOAT: *out_float++ = left * _g; //< @r-lyeh: added master gain *out_float++ = right * _g; //< @r-lyeh: added master gain break; } } } #endif // STS_MIXER_IMPLEMENTATION //////////////////////////////////////////////////////////////////////////////// // EXAMPLE // This is a very simple example loading a stream and a sample using // dr_flac.h (https://github.com/mackron/dr_libs) and SDL2. You can of course also use stb_vorbis or something similar :) // Please note how the audio thread of SDL2 will be locked when the mixer state get's modified. This is important! // Also there's no error checking in the entire example code, so beware. // #if 0 #include "SDL.h" #define DR_FLAC_IMPLEMENTATION #include "dr_flac.h" #define STS_MIXER_IMPLEMENTATION #include "sts_mixer.h" SDL_AudioDeviceID audio_device = 0; sts_mixer_t mixer; // encapsulate drflac and some buffer with the sts_mixer_stream_t typedef struct { drflac* flac; // FLAC decoder state sts_mixer_stream_t stream; // mixer stream int32_t data[4096*2]; // static sample buffer } mystream_t; // SDL2 audio callback static void audio_callback(void* userdata, Uint8* stream, int len) { (void)(userdata); sts_mixer_mix_audio(&mixer, stream, len / (sizeof(int) * 2)); } // load a sample static void load_sample(sts_mixer_sample_t* sample, const char *filename) { drflac* flac = drflac_open_file(filename); sample->frequency = flac->sampleRate; sample->audio_format = STS_MIXER_SAMPLE_FORMAT_32; sample->length = flac->totalSampleCount; sample->data = malloc(sample->length * sizeof(int32_t)); drflac_read_s32(flac, sample->length, (int32_t*)sample->data); drflac_close(flac); } // the callback to refill the stream data static void refill_stream(sts_mixer_sample_t* sample, void* userdata) { mystream_t* stream = (mystream_t*)userdata; if (drflac_read_s32(stream->flac, sample->length, stream->data) < sample->length) drflac_seek_to_sample(stream->flac, 0); } // load a stream static void load_stream(mystream_t* stream, const char *filename) { stream->flac = drflac_open_file(filename); stream->stream.userdata = stream; stream->stream.callback = refill_stream; stream->stream.sample.frequency = stream->flac->sampleRate; stream->stream.sample.audio_format = STS_MIXER_SAMPLE_FORMAT_32; stream->stream.sample.length = 4096*2; stream->stream.sample.data = stream->data; refill_stream(&stream->stream.sample, stream); } // helper to get random [0.0f..1.0f values static float randf() { return (float)(rand()) / (float)RAND_MAX; } int main(int argc, char *argv[]) { SDL_AudioSpec want, have; sts_mixer_sample_t sample; mystream_t stream; (void)(argc); (void)(argv); // init SDL2 + audio want.format = AUDIO_S32SYS; want.freq = 44100; want.channels = 2; want.userdata = NULL; want.samples = 4096; want.callback = audio_callback; SDL_Init(SDL_INIT_AUDIO); audio_device = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); // init sts_mixer and load things sts_mixer_init(&mixer, 44100, STS_MIXER_SAMPLE_FORMAT_32); load_sample(&sample, "effect.flac"); load_stream(&stream, "music.flac"); // play the stream sts_mixer_play_stream(&mixer, &stream.stream, 0.7f); // start audio processing and do a loop for audio effects SDL_PauseAudioDevice(audio_device, 0); for (;;) { // !!!IMPORTANT!!! lock the audio thread before modifying data in the sts_mixer !!! SDL_LockAudioDevice(audio_device); // play a sample with random gain, pitch and panning sts_mixer_play_sample(&mixer, &sample, randf(), 0.5f + randf(), -1.0f + randf() * 2.0f); // unlock audio thread again SDL_UnlockAudioDevice(audio_device); // wait ... SDL_Delay(76); } SDL_PauseAudioDevice(audio_device, 1); SDL_CloseAudioDevice(audio_device); SDL_Quit(); return 0; } #endif // 0 /* This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to */