diff --git a/CMakeLists.txt b/CMakeLists.txt index 5244782..9b84331 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -179,7 +179,10 @@ add_library(boo lib/inputdev/DeviceBase.cpp include/boo/inputdev/DeviceBase.hpp lib/inputdev/DeviceSignature.cpp include/boo/inputdev/DeviceSignature.hpp lib/inputdev/IHIDDevice.hpp + lib/audiodev/AudioMatrix.hpp lib/audiodev/AudioMatrix.cpp + lib/audiodev/AudioVoiceEngine.hpp + lib/audiodev/AudioVoiceEngine.cpp lib/audiodev/AudioVoice.hpp lib/audiodev/AudioVoice.cpp include/boo/inputdev/IHIDListener.hpp @@ -187,7 +190,7 @@ add_library(boo include/boo/graphicsdev/IGraphicsDataFactory.hpp include/boo/graphicsdev/IGraphicsCommandQueue.hpp include/boo/audiodev/IAudioVoice.hpp - include/boo/audiodev/IAudioVoiceAllocator.hpp + include/boo/audiodev/IAudioVoiceEngine.hpp include/boo/IWindow.hpp include/boo/IApplication.hpp include/boo/System.hpp diff --git a/include/boo/audiodev/AudioMatrix.hpp b/include/boo/audiodev/AudioMatrix.hpp deleted file mode 100644 index 2069de4..0000000 --- a/include/boo/audiodev/AudioMatrix.hpp +++ /dev/null @@ -1,56 +0,0 @@ -#ifndef BOO_AUDIOMATRIX_HPP -#define BOO_AUDIOMATRIX_HPP - -#include "IAudioVoice.hpp" -#include -#include - -namespace boo -{ - -class AudioMatrixMono -{ - AudioChannelSet m_setOut = AudioChannelSet::Stereo; - float m_coefs[8]; - std::vector m_interleaveBuf; -public: - AudioMatrixMono() {setDefaultMatrixCoefficients();} - - AudioChannelSet audioChannelSet() const {return m_setOut;} - void setAudioChannelSet(AudioChannelSet set) {m_setOut = set;} - void setDefaultMatrixCoefficients(); - void setMatrixCoefficients(const float coefs[8]) - { - for (int i=0 ; i<8 ; ++i) - m_coefs[i] = coefs[i]; - } - - void bufferMonoSampleData(IAudioVoice& voice, const int16_t* data, size_t samples); -}; - -class AudioMatrixStereo -{ - AudioChannelSet m_setOut = AudioChannelSet::Stereo; - float m_coefs[8][2]; - std::vector m_interleaveBuf; -public: - AudioMatrixStereo() {setDefaultMatrixCoefficients();} - - AudioChannelSet audioChannelSet() const {return m_setOut;} - void setAudioChannelSet(AudioChannelSet set) {m_setOut = set;} - void setDefaultMatrixCoefficients(); - void setMatrixCoefficients(const float coefs[8][2]) - { - for (int i=0 ; i<8 ; ++i) - { - m_coefs[i][0] = coefs[i][0]; - m_coefs[i][1] = coefs[i][1]; - } - } - - void bufferStereoSampleData(IAudioVoice& voice, const int16_t* data, size_t frames); -}; - -} - -#endif // BOO_AUDIOMATRIX_HPP diff --git a/include/boo/audiodev/IAudioVoice.hpp b/include/boo/audiodev/IAudioVoice.hpp index f1c9b76..6c02d1f 100644 --- a/include/boo/audiodev/IAudioVoice.hpp +++ b/include/boo/audiodev/IAudioVoice.hpp @@ -56,18 +56,33 @@ struct IAudioVoice { virtual ~IAudioVoice() = default; - /** Get voice's actual channel-map based on client request and HW capabilities */ - virtual const ChannelMap& channelMap() const=0; + /** Reset channel-gains to voice defaults */ + virtual void setDefaultMatrixCoefficients()=0; - /** Called by client in response to IAudioVoiceCallback::needsNextBuffer() - * Supplying channel-interleaved sample data */ - virtual void bufferSampleData(const int16_t* data, size_t frames)=0; + /** Set channel-gains for mono audio source (AudioChannel enum for array index) */ + virtual void setMonoMatrixCoefficients(const float coefs[8])=0; + + /** Set channel-gains for stereo audio source (AudioChannel enum for array index) */ + virtual void setStereoMatrixCoefficients(const float coefs[8][2])=0; + + /** Called by client to dynamically adjust the pitch of voices with dynamic pitch enabled */ + virtual void setPitchRatio(double ratio)=0; /** Instructs platform to begin consuming sample data; invoking callback as needed */ virtual void start()=0; /** Instructs platform to stop consuming sample data */ virtual void stop()=0; + + /** Invalidates this voice by removing it from the AudioVoiceEngine */ + virtual void unbindVoice()=0; +}; + +struct IAudioVoiceCallback +{ + /** boo calls this on behalf of the audio platform to request more audio + * frames from the client */ + virtual size_t supplyAudio(IAudioVoice& voice, size_t frames, int16_t* data)=0; }; } diff --git a/include/boo/audiodev/IAudioVoiceAllocator.hpp b/include/boo/audiodev/IAudioVoiceAllocator.hpp deleted file mode 100644 index 2e79835..0000000 --- a/include/boo/audiodev/IAudioVoiceAllocator.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef BOO_IAUDIOVOICEALLOCATOR_HPP -#define BOO_IAUDIOVOICEALLOCATOR_HPP - -#include "IAudioVoice.hpp" -#include - -namespace boo -{ - -struct IAudioVoiceCallback -{ - /** boo calls this on behalf of the audio platform to request more audio - * frames from the client */ - virtual void needsNextBuffer(IAudioVoice& voice, size_t frames)=0; -}; - -struct IAudioVoiceAllocator -{ - virtual ~IAudioVoiceAllocator() = default; - - /** Client calls this to request allocation of new mixer-voice. - * Returns empty unique_ptr if necessary resources aren't available. - * ChannelLayout automatically reduces to maximum-supported layout by HW. - * - * Client must be prepared to supply audio frames via the callback when this is called; - * the backing audio-buffers are primed with initial data for low-latency playback start */ - virtual std::unique_ptr allocateNewVoice(AudioChannelSet layoutOut, - unsigned sampleRate, - IAudioVoiceCallback* cb)=0; - - /** Client may use this to determine current speaker-setup */ - virtual AudioChannelSet getAvailableSet()=0; - - /** Ensure all voices' platform buffers are filled as much as possible */ - virtual void pumpVoices()=0; -}; - -/** Obtain host platform's voice allocator */ -std::unique_ptr NewAudioVoiceAllocator(); - -} - -#endif // BOO_IAUDIOVOICEALLOCATOR_HPP diff --git a/include/boo/audiodev/IAudioVoiceEngine.hpp b/include/boo/audiodev/IAudioVoiceEngine.hpp new file mode 100644 index 0000000..514ebc9 --- /dev/null +++ b/include/boo/audiodev/IAudioVoiceEngine.hpp @@ -0,0 +1,43 @@ +#ifndef BOO_IAUDIOVOICEENGINE_HPP +#define BOO_IAUDIOVOICEENGINE_HPP + +#include "IAudioVoice.hpp" +#include + +namespace boo +{ + +/** Mixing and sample-rate-conversion system. Allocates voices and mixes them + * before sending the final samples to an OS-supplied audio-queue */ +struct IAudioVoiceEngine +{ + virtual ~IAudioVoiceEngine() = default; + + /** Client calls this to request allocation of new mixer-voice. + * Returns empty unique_ptr if necessary resources aren't available. + * ChannelLayout automatically reduces to maximum-supported layout by HW. + * + * Client must be prepared to supply audio frames via the callback when this is called; + * the backing audio-buffers are primed with initial data for low-latency playback start */ + virtual std::unique_ptr allocateNewMonoVoice(double sampleRate, + IAudioVoiceCallback* cb, + bool dynamicPitch=false)=0; + + /** Same as allocateNewMonoVoice, but source audio is stereo-interleaved */ + virtual std::unique_ptr allocateNewStereoVoice(double sampleRate, + IAudioVoiceCallback* cb, + bool dynamicPitch=false)=0; + + /** Client may use this to determine current speaker-setup */ + virtual AudioChannelSet getAvailableSet()=0; + + /** Ensure backing platform buffer is filled as much as possible with mixed samples */ + virtual void pumpAndMixVoices()=0; +}; + +/** Construct host platform's voice engine */ +std::unique_ptr NewAudioVoiceEngine(); + +} + +#endif // BOO_IAUDIOVOICEENGINE_HPP diff --git a/lib/audiodev/ALSA.cpp b/lib/audiodev/ALSA.cpp index 234bb2d..5b0f761 100644 --- a/lib/audiodev/ALSA.cpp +++ b/lib/audiodev/ALSA.cpp @@ -1,217 +1,36 @@ #include #include -#include "boo/audiodev/IAudioVoiceAllocator.hpp" +#include "AudioVoiceEngine.hpp" #include "logvisor/logvisor.hpp" #include -#include namespace boo { static logvisor::Module Log("boo::ALSA"); -struct ALSAAudioVoiceAllocator; -struct ALSAAudioVoice : IAudioVoice +struct ALSAAudioVoiceEngine : BaseAudioVoiceEngine { - ALSAAudioVoiceAllocator& m_parent; - std::list::iterator m_parentIt; - - ChannelMap m_map; - IAudioVoiceCallback* m_cb; - snd_pcm_t* m_pcm = nullptr; + snd_pcm_t* m_pcm; snd_pcm_uframes_t m_bufSize; snd_pcm_uframes_t m_periodSize; - const ChannelMap& channelMap() const {return m_map;} + std::vector m_final16; + std::vector m_final32; + std::vector m_finalFlt; - ALSAAudioVoice(ALSAAudioVoiceAllocator& parent, AudioChannelSet set, - unsigned sampleRate, IAudioVoiceCallback* cb) - : m_parent(parent), m_cb(cb) + ~ALSAAudioVoiceEngine() { - if (snd_pcm_open(&m_pcm, "default", SND_PCM_STREAM_PLAYBACK, SND_PCM_ASYNC) < 0) - { - Log.report(logvisor::Error, "unable to allocate ALSA voice"); - return; - } - - unsigned chCount = ChannelCount(set); - int err; - while ((err = snd_pcm_set_params(m_pcm, SND_PCM_FORMAT_S16, SND_PCM_ACCESS_RW_INTERLEAVED, - chCount, sampleRate, 1, 100000)) < 0) - { - if (set == AudioChannelSet::Stereo) - break; - set = AudioChannelSet(int(set) - 1); - chCount = ChannelCount(set); - } - if (err < 0) - { - snd_pcm_close(m_pcm); - m_pcm = nullptr; - Log.report(logvisor::Error, "unable to set ALSA voice params"); - return; - } + snd_pcm_drain(m_pcm); + snd_pcm_close(m_pcm); + } + AudioChannelSet _getAvailableSet() + { snd_pcm_chmap_query_t** chmaps = snd_pcm_query_chmaps(m_pcm); - if (chmaps) - { - snd_pcm_chmap_t* foundChmap = nullptr; - for (snd_pcm_chmap_query_t** chmap = chmaps ; *chmap != nullptr ; ++chmap) - { - if ((*chmap)->map.channels == chCount) - { - snd_pcm_chmap_t* chm = &(*chmap)->map; - uint64_t chBits = 0; - for (int c=0 ; cchannels ; ++c) - chBits |= 1 << chm->pos[c]; - - bool good = false; - switch (set) - { - case AudioChannelSet::Stereo: - if ((chBits & (1 << SND_CHMAP_FL)) != 0 && - (chBits & (1 << SND_CHMAP_FR)) != 0) - good = true; - break; - case AudioChannelSet::Quad: - if ((chBits & (1 << SND_CHMAP_FL)) != 0 && - (chBits & (1 << SND_CHMAP_FR)) != 0 && - (chBits & (1 << SND_CHMAP_RL)) != 0 && - (chBits & (1 << SND_CHMAP_RR)) != 0) - good = true; - break; - case AudioChannelSet::Surround51: - if ((chBits & (1 << SND_CHMAP_FL)) != 0 && - (chBits & (1 << SND_CHMAP_FR)) != 0 && - (chBits & (1 << SND_CHMAP_RL)) != 0 && - (chBits & (1 << SND_CHMAP_RR)) != 0 && - (chBits & (1 << SND_CHMAP_FC)) != 0 && - (chBits & (1 << SND_CHMAP_LFE)) != 0) - good = true; - break; - case AudioChannelSet::Surround71: - if ((chBits & (1 << SND_CHMAP_FL)) != 0 && - (chBits & (1 << SND_CHMAP_FR)) != 0 && - (chBits & (1 << SND_CHMAP_RL)) != 0 && - (chBits & (1 << SND_CHMAP_RR)) != 0 && - (chBits & (1 << SND_CHMAP_FC)) != 0 && - (chBits & (1 << SND_CHMAP_LFE)) != 0 && - (chBits & (1 << SND_CHMAP_SL)) != 0 && - (chBits & (1 << SND_CHMAP_SR)) != 0) - good = true; - break; - default: break; - } - - if (good) - { - foundChmap = chm; - break; - } - } - } - - if (!foundChmap) - { - snd_pcm_close(m_pcm); - m_pcm = nullptr; - snd_pcm_free_chmaps(chmaps); - Log.report(logvisor::Error, "unable to find matching ALSA voice chmap"); - return; - } - m_map.m_channelCount = chCount; - for (int c=0 ; cchannels ; ++c) - m_map.m_channels[c] = AudioChannel(foundChmap->pos[c] - 3); - snd_pcm_set_chmap(m_pcm, foundChmap); - snd_pcm_free_chmaps(chmaps); - } - else - { - m_map.m_channelCount = 2; - m_map.m_channels[0] = AudioChannel::FrontLeft; - m_map.m_channels[1] = AudioChannel::FrontRight; - } - - snd_pcm_get_params(m_pcm, &m_bufSize, &m_periodSize); - snd_pcm_prepare(m_pcm); - - pump(); - } - - ~ALSAAudioVoice(); - - void bufferSampleData(const int16_t* data, size_t frames) - { - if (m_pcm) - snd_pcm_writei(m_pcm, data, frames); - } - - void start() - { - if (m_pcm) - snd_pcm_start(m_pcm); - } - - void stop() - { - if (m_pcm) - snd_pcm_drain(m_pcm); - } - - void pump() - { - snd_pcm_sframes_t frames = snd_pcm_avail(m_pcm); - if (frames < 0) - { - snd_pcm_state_t st = snd_pcm_state(m_pcm); - if (st == SND_PCM_STATE_XRUN) - { - snd_pcm_prepare(m_pcm); - frames = snd_pcm_avail(m_pcm); - fprintf(stderr, "REC %ld\n", frames); - } - else - return; - } - if (frames < 0) - return; - snd_pcm_sframes_t buffers = frames / m_periodSize; - for (snd_pcm_sframes_t b=0 ; bneedsNextBuffer(*this, m_periodSize); - } -}; - -struct ALSAAudioVoiceAllocator : IAudioVoiceAllocator -{ - std::list m_allocatedVoices; - - std::unique_ptr allocateNewVoice(AudioChannelSet layoutOut, - unsigned sampleRate, - IAudioVoiceCallback* cb) - { - ALSAAudioVoice* newVoice = new ALSAAudioVoice(*this, layoutOut, sampleRate, cb); - newVoice->m_parentIt = m_allocatedVoices.insert(m_allocatedVoices.end(), newVoice); - std::unique_ptr ret(newVoice); - if (!newVoice->m_pcm) - return {}; - return ret; - } - - AudioChannelSet getAvailableSet() - { - snd_pcm_t* pcm; - if (snd_pcm_open(&pcm, "default", SND_PCM_STREAM_PLAYBACK, SND_PCM_ASYNC) < 0) - { - Log.report(logvisor::Error, "unable to allocate ALSA voice"); - return AudioChannelSet::Unknown; - } - - snd_pcm_chmap_query_t** chmaps = snd_pcm_query_chmaps(pcm); if (!chmaps) - { - snd_pcm_close(pcm); return AudioChannelSet::Stereo; - } + static const std::array testSets = {AudioChannelSet::Surround71, AudioChannelSet::Surround51, AudioChannelSet::Quad, AudioChannelSet::Stereo}; @@ -279,23 +98,242 @@ struct ALSAAudioVoiceAllocator : IAudioVoiceAllocator return AudioChannelSet::Unknown; } - void pumpVoices() + AudioVoiceEngineMixInfo _getEngineMixInfo() { - for (ALSAAudioVoice* vox : m_allocatedVoices) - vox->pump(); + if (snd_pcm_open(&m_pcm, "default", SND_PCM_STREAM_PLAYBACK, 0) < 0) + { + Log.report(logvisor::Error, "unable to allocate ALSA voice"); + return {}; + } + + AudioVoiceEngineMixInfo ret = {}; + + /* Query audio card for best supported format amd sample-rate */ + snd_pcm_hw_params_t* hwParams; + snd_pcm_hw_params_malloc(&hwParams); + snd_pcm_hw_params_any(m_pcm, hwParams); + + snd_pcm_format_t bestFmt; + if (!snd_pcm_hw_params_test_format(m_pcm, hwParams, SND_PCM_FORMAT_S32)) + { + bestFmt = SND_PCM_FORMAT_S32; + ret.m_sampleFormat = SOXR_INT32_I; + ret.m_bitsPerSample = 32; + } + else if (!snd_pcm_hw_params_test_format(m_pcm, hwParams, SND_PCM_FORMAT_S16)) + { + bestFmt = SND_PCM_FORMAT_S16; + ret.m_sampleFormat = SOXR_INT16_I; + ret.m_bitsPerSample = 16; + } + else + { + snd_pcm_close(m_pcm); + m_pcm = nullptr; + Log.report(logvisor::Fatal, "unsupported audio formats on default ALSA device"); + return {}; + } + + unsigned int bestRate; + if (!snd_pcm_hw_params_test_rate(m_pcm, hwParams, 96000, 0)) + { + bestRate = 96000; + ret.m_sampleRate = 96000.0; + } + else if (!snd_pcm_hw_params_test_rate(m_pcm, hwParams, 48000, 0)) + { + bestRate = 48000; + ret.m_sampleRate = 48000.0; + } + else + { + snd_pcm_close(m_pcm); + m_pcm = nullptr; + Log.report(logvisor::Fatal, "unsupported audio sample rates on default ALSA device"); + return {}; + } + + snd_pcm_hw_params_free(hwParams); + + /* Query audio card for channel map */ + ret.m_channels = _getAvailableSet(); + + /* Populate channel map */ + unsigned chCount = ChannelCount(ret.m_channels); + int err; + while ((err = snd_pcm_set_params(m_pcm, bestFmt, SND_PCM_ACCESS_RW_INTERLEAVED, + chCount, bestRate, 0, 100000)) < 0) + { + if (ret.m_channels == AudioChannelSet::Stereo) + break; + ret.m_channels = AudioChannelSet(int(ret.m_channels) - 1); + chCount = ChannelCount(ret.m_channels); + } + if (err < 0) + { + snd_pcm_close(m_pcm); + m_pcm = nullptr; + Log.report(logvisor::Error, "unable to set ALSA voice params"); + return {}; + } + + snd_pcm_chmap_query_t** chmaps = snd_pcm_query_chmaps(m_pcm); + ChannelMap& chmapOut = ret.m_channelMap; + if (chmaps) + { + snd_pcm_chmap_t* foundChmap = nullptr; + for (snd_pcm_chmap_query_t** chmap = chmaps ; *chmap != nullptr ; ++chmap) + { + if ((*chmap)->map.channels == chCount) + { + snd_pcm_chmap_t* chm = &(*chmap)->map; + uint64_t chBits = 0; + for (int c=0 ; cchannels ; ++c) + chBits |= 1 << chm->pos[c]; + + bool good = false; + switch (ret.m_channels) + { + case AudioChannelSet::Stereo: + if ((chBits & (1 << SND_CHMAP_FL)) != 0 && + (chBits & (1 << SND_CHMAP_FR)) != 0) + good = true; + break; + case AudioChannelSet::Quad: + if ((chBits & (1 << SND_CHMAP_FL)) != 0 && + (chBits & (1 << SND_CHMAP_FR)) != 0 && + (chBits & (1 << SND_CHMAP_RL)) != 0 && + (chBits & (1 << SND_CHMAP_RR)) != 0) + good = true; + break; + case AudioChannelSet::Surround51: + if ((chBits & (1 << SND_CHMAP_FL)) != 0 && + (chBits & (1 << SND_CHMAP_FR)) != 0 && + (chBits & (1 << SND_CHMAP_RL)) != 0 && + (chBits & (1 << SND_CHMAP_RR)) != 0 && + (chBits & (1 << SND_CHMAP_FC)) != 0 && + (chBits & (1 << SND_CHMAP_LFE)) != 0) + good = true; + break; + case AudioChannelSet::Surround71: + if ((chBits & (1 << SND_CHMAP_FL)) != 0 && + (chBits & (1 << SND_CHMAP_FR)) != 0 && + (chBits & (1 << SND_CHMAP_RL)) != 0 && + (chBits & (1 << SND_CHMAP_RR)) != 0 && + (chBits & (1 << SND_CHMAP_FC)) != 0 && + (chBits & (1 << SND_CHMAP_LFE)) != 0 && + (chBits & (1 << SND_CHMAP_SL)) != 0 && + (chBits & (1 << SND_CHMAP_SR)) != 0) + good = true; + break; + default: break; + } + + if (good) + { + foundChmap = chm; + break; + } + } + } + + if (!foundChmap) + { + snd_pcm_close(m_pcm); + m_pcm = nullptr; + snd_pcm_free_chmaps(chmaps); + Log.report(logvisor::Error, "unable to find matching ALSA voice chmap"); + return {}; + } + chmapOut.m_channelCount = chCount; + for (int c=0 ; cchannels ; ++c) + chmapOut.m_channels[c] = AudioChannel(foundChmap->pos[c] - 3); + snd_pcm_set_chmap(m_pcm, foundChmap); + snd_pcm_free_chmaps(chmaps); + } + else + { + chmapOut.m_channelCount = 2; + chmapOut.m_channels[0] = AudioChannel::FrontLeft; + chmapOut.m_channels[1] = AudioChannel::FrontRight; + } + + return ret; + } + + ALSAAudioVoiceEngine() + : BaseAudioVoiceEngine(std::bind(&ALSAAudioVoiceEngine::_getEngineMixInfo, this)) + { + /* Base class will call _getEngineMixInfo first */ + snd_pcm_get_params(m_pcm, &m_bufSize, &m_periodSize); + snd_pcm_prepare(m_pcm); + m_mixInfo.m_periodFrames = m_periodSize; + + /* Allocate master mix space */ + switch (m_mixInfo.m_sampleFormat) + { + case SOXR_INT16_I: + m_final16.resize(m_periodSize * m_mixInfo.m_channelMap.m_channelCount); + break; + case SOXR_INT32_I: + m_final32.resize(m_periodSize * m_mixInfo.m_channelMap.m_channelCount); + break; + case SOXR_FLOAT32_I: + m_finalFlt.resize(m_periodSize * m_mixInfo.m_channelMap.m_channelCount); + break; + default: + break; + } + } + + void pumpAndMixVoices() + { + snd_pcm_sframes_t frames = snd_pcm_avail_update(m_pcm); + if (frames < 0) + { + snd_pcm_state_t st = snd_pcm_state(m_pcm); + if (st == SND_PCM_STATE_XRUN) + { + snd_pcm_prepare(m_pcm); + frames = snd_pcm_avail_update(m_pcm); + Log.report(logvisor::Warning, "ALSA underrun %ld frames", frames); + } + else + return; + } + if (frames < 0) + return; + + snd_pcm_sframes_t buffers = frames / m_periodSize; + for (snd_pcm_sframes_t b=0 ; b NewAudioVoiceEngine() { - if (m_pcm) - snd_pcm_close(m_pcm); - m_parent.m_allocatedVoices.erase(m_parentIt); -} - -std::unique_ptr NewAudioVoiceAllocator() -{ - return std::make_unique(); + std::unique_ptr ret = std::make_unique(); + if (!static_cast(*ret).m_pcm) + return {}; + return ret; } } diff --git a/lib/audiodev/AQS.cpp b/lib/audiodev/AQS.cpp index 093a64c..9bc0239 100644 --- a/lib/audiodev/AQS.cpp +++ b/lib/audiodev/AQS.cpp @@ -217,7 +217,7 @@ struct AQSAudioVoice : IAudioVoice } }; -struct AQSAudioVoiceAllocator : IAudioVoiceAllocator +struct AQSAudioVoiceAllocator : IAudioVoiceEngine { static void DummyCallback(void* inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) {} @@ -283,7 +283,7 @@ struct AQSAudioVoiceAllocator : IAudioVoiceAllocator void pumpVoices() {} }; -std::unique_ptr NewAudioVoiceAllocator() +std::unique_ptr NewAudioVoiceAllocator() { return std::make_unique(); } diff --git a/lib/audiodev/AudioMatrix.cpp b/lib/audiodev/AudioMatrix.cpp index e021406..aecc259 100644 --- a/lib/audiodev/AudioMatrix.cpp +++ b/lib/audiodev/AudioMatrix.cpp @@ -1,13 +1,42 @@ -#include "boo/audiodev/AudioMatrix.hpp" +#include "AudioMatrix.hpp" +#include "AudioVoiceEngine.hpp" #include +#include namespace boo { -void AudioMatrixMono::setDefaultMatrixCoefficients() +static inline int16_t Clamp16(float in) +{ + if (in < SHRT_MIN) + return SHRT_MIN; + else if (in > SHRT_MAX) + return SHRT_MAX; + return in; +} + +static inline int32_t Clamp32(float in) +{ + if (in < INT_MIN) + return INT_MIN; + else if (in > INT_MAX) + return INT_MAX; + return in; +} + +static inline float ClampFlt(float in) +{ + if (in < -1.f) + return -1.f; + else if (in > 1.f) + return 1.f; + return in; +} + +void AudioMatrixMono::setDefaultMatrixCoefficients(AudioChannelSet acSet) { memset(m_coefs, 0, sizeof(m_coefs)); - switch (m_setOut) + switch (acSet) { case AudioChannelSet::Stereo: case AudioChannelSet::Quad: @@ -22,27 +51,58 @@ void AudioMatrixMono::setDefaultMatrixCoefficients() } } -void AudioMatrixMono::bufferMonoSampleData(IAudioVoice& voice, const int16_t* data, size_t samples) +void AudioMatrixMono::mixMonoSampleData(const AudioVoiceEngineMixInfo& info, + const int16_t* dataIn, int16_t* dataOut, size_t samples) const { - const ChannelMap& chmap = voice.channelMap(); - m_interleaveBuf.clear(); - m_interleaveBuf.reserve(samples * chmap.m_channelCount); - for (size_t s=0 ; s +#include + +namespace boo +{ +struct AudioVoiceEngineMixInfo; + +class AudioMatrixMono +{ + float m_coefs[8]; +public: + AudioMatrixMono() {setDefaultMatrixCoefficients(AudioChannelSet::Stereo);} + + void setDefaultMatrixCoefficients(AudioChannelSet acSet); + void setMatrixCoefficients(const float coefs[8]) + { + for (int i=0 ; i<8 ; ++i) + m_coefs[i] = coefs[i]; + } + + void mixMonoSampleData(const AudioVoiceEngineMixInfo& info, + const int16_t* dataIn, int16_t* dataOut, size_t samples) const; + void mixMonoSampleData(const AudioVoiceEngineMixInfo& info, + const int32_t* dataIn, int32_t* dataOut, size_t samples) const; + void mixMonoSampleData(const AudioVoiceEngineMixInfo& info, + const float* dataIn, float* dataOut, size_t samples) const; +}; + +class AudioMatrixStereo +{ + float m_coefs[8][2]; +public: + AudioMatrixStereo() {setDefaultMatrixCoefficients(AudioChannelSet::Stereo);} + + void setDefaultMatrixCoefficients(AudioChannelSet acSet); + void setMatrixCoefficients(const float coefs[8][2]) + { + for (int i=0 ; i<8 ; ++i) + { + m_coefs[i][0] = coefs[i][0]; + m_coefs[i][1] = coefs[i][1]; + } + } + + void mixStereoSampleData(const AudioVoiceEngineMixInfo& info, + const int16_t* dataIn, int16_t* dataOut, size_t frames) const; + void mixStereoSampleData(const AudioVoiceEngineMixInfo& info, + const int32_t* dataIn, int32_t* dataOut, size_t frames) const; + void mixStereoSampleData(const AudioVoiceEngineMixInfo& info, + const float* dataIn, float* dataOut, size_t frames) const; +}; + +} + +#endif // BOO_AUDIOMATRIX_HPP diff --git a/lib/audiodev/AudioVoice.cpp b/lib/audiodev/AudioVoice.cpp index e69de29..78cbe73 100644 --- a/lib/audiodev/AudioVoice.cpp +++ b/lib/audiodev/AudioVoice.cpp @@ -0,0 +1,239 @@ +#include "AudioVoice.hpp" +#include "AudioVoiceEngine.hpp" +#include "logvisor/logvisor.hpp" + +namespace boo +{ +static logvisor::Module Log("boo::AudioVoice"); + +static std::vector scratchIn; +static std::vector scratch16; +static std::vector scratch32; +static std::vector scratchFlt; + +AudioVoice::AudioVoice(BaseAudioVoiceEngine& parent, IAudioVoiceCallback* cb, bool dynamicRate) +: m_parent(parent), m_cb(cb), m_dynamicRate(dynamicRate) {} + +AudioVoice::~AudioVoice() +{ + unbindVoice(); + soxr_delete(m_src); +} + +void AudioVoice::setPitchRatio(double ratio) +{ + if (m_dynamicRate) + { + soxr_error_t err = soxr_set_io_ratio(m_src, ratio, m_parent.mixInfo().m_periodFrames); + if (err) + { + Log.report(logvisor::Fatal, "unable to set resampler rate: %s", soxr_strerror(err)); + return; + } + } +} + +void AudioVoice::start() +{ + m_running = true; +} + +void AudioVoice::stop() +{ + m_running = false; +} + +void AudioVoice::unbindVoice() +{ + if (m_bound) + { + m_parent.m_activeVoices.erase(m_parentIt); + m_bound = false; + } +} + +AudioVoiceMono::AudioVoiceMono(BaseAudioVoiceEngine& parent, IAudioVoiceCallback* cb, + double sampleRate, bool dynamicRate) +: AudioVoice(parent, cb, dynamicRate) +{ + soxr_io_spec_t ioSpec = soxr_io_spec(SOXR_INT16_I, parent.mixInfo().m_sampleFormat); + soxr_quality_spec_t qSpec = soxr_quality_spec(SOXR_20_BITQ, dynamicRate ? SOXR_VR : 0); + + soxr_error_t err; + m_src = soxr_create(sampleRate, parent.mixInfo().m_sampleRate, 1, + &err, &ioSpec, &qSpec, nullptr); + + if (err) + { + Log.report(logvisor::Fatal, "unable to create soxr resampler: %s", soxr_strerror(err)); + return; + } + + soxr_set_input_fn(m_src, soxr_input_fn_t(SRCCallback), this, 0); +} + +size_t AudioVoiceMono::SRCCallback(AudioVoiceMono* ctx, int16_t** data, size_t frames) +{ + if (scratchIn.size() < frames) + scratchIn.resize(frames); + *data = scratchIn.data(); + return ctx->m_cb->supplyAudio(*ctx, frames, scratchIn.data()); +} + +size_t AudioVoiceMono::pumpAndMix(const AudioVoiceEngineMixInfo& mixInfo, + size_t frames, int16_t* buf) +{ + if (scratch16.size() < frames) + scratch16.resize(frames); + + size_t oDone = soxr_output(m_src, scratch16.data(), frames); + + m_matrix.mixMonoSampleData(mixInfo, scratch16.data(), buf, oDone); + return oDone; +} + +size_t AudioVoiceMono::pumpAndMix(const AudioVoiceEngineMixInfo& mixInfo, + size_t frames, int32_t* buf) +{ + if (scratch32.size() < frames) + scratch32.resize(frames); + + size_t oDone = soxr_output(m_src, scratch32.data(), frames); + + m_matrix.mixMonoSampleData(mixInfo, scratch32.data(), buf, oDone); + return oDone; +} + +size_t AudioVoiceMono::pumpAndMix(const AudioVoiceEngineMixInfo& mixInfo, + size_t frames, float* buf) +{ + if (scratchFlt.size() < frames) + scratchFlt.resize(frames); + + size_t oDone = soxr_output(m_src, scratchFlt.data(), frames); + + m_matrix.mixMonoSampleData(mixInfo, scratchFlt.data(), buf, oDone); + return oDone; +} + +void AudioVoiceMono::setDefaultMatrixCoefficients() +{ + m_matrix.setDefaultMatrixCoefficients(m_parent.mixInfo().m_channels); +} + +void AudioVoiceMono::setMonoMatrixCoefficients(const float coefs[8]) +{ + m_matrix.setMatrixCoefficients(coefs); +} + +void AudioVoiceMono::setStereoMatrixCoefficients(const float coefs[8][2]) +{ + float newCoefs[8] = + { + coefs[0][0], + coefs[1][0], + coefs[2][0], + coefs[3][0], + coefs[4][0], + coefs[5][0], + coefs[6][0], + coefs[7][0] + }; + m_matrix.setMatrixCoefficients(newCoefs); +} + +AudioVoiceStereo::AudioVoiceStereo(BaseAudioVoiceEngine& parent, IAudioVoiceCallback* cb, + double sampleRate, bool dynamicRate) +: AudioVoice(parent, cb, dynamicRate) +{ + soxr_io_spec_t ioSpec = soxr_io_spec(SOXR_INT16_I, parent.mixInfo().m_sampleFormat); + soxr_quality_spec_t qSpec = soxr_quality_spec(SOXR_20_BITQ, dynamicRate ? SOXR_VR : 0); + + soxr_error_t err; + m_src = soxr_create(sampleRate, parent.mixInfo().m_sampleRate, 2, + &err, &ioSpec, &qSpec, nullptr); + + if (!m_src) + { + Log.report(logvisor::Fatal, "unable to create soxr resampler: %s", soxr_strerror(err)); + return; + } + + soxr_set_input_fn(m_src, soxr_input_fn_t(SRCCallback), this, 0); +} + +size_t AudioVoiceStereo::SRCCallback(AudioVoiceStereo* ctx, int16_t** data, size_t frames) +{ + size_t samples = frames * 2; + if (scratchIn.size() < samples) + scratchIn.resize(samples); + *data = scratchIn.data(); + return ctx->m_cb->supplyAudio(*ctx, frames, scratchIn.data()); +} + +size_t AudioVoiceStereo::pumpAndMix(const AudioVoiceEngineMixInfo& mixInfo, + size_t frames, int16_t* buf) +{ + size_t samples = frames * 2; + if (scratch16.size() < samples) + scratch16.resize(samples); + + size_t oDone = soxr_output(m_src, scratch16.data(), frames); + + m_matrix.mixStereoSampleData(mixInfo, scratch16.data(), buf, oDone); + return oDone; +} + +size_t AudioVoiceStereo::pumpAndMix(const AudioVoiceEngineMixInfo& mixInfo, + size_t frames, int32_t* buf) +{ + size_t samples = frames * 2; + if (scratch32.size() < samples) + scratch32.resize(samples); + + size_t oDone = soxr_output(m_src, scratch32.data(), frames); + + m_matrix.mixStereoSampleData(mixInfo, scratch32.data(), buf, oDone); + return oDone; +} + +size_t AudioVoiceStereo::pumpAndMix(const AudioVoiceEngineMixInfo& mixInfo, + size_t frames, float* buf) +{ + size_t samples = frames * 2; + if (scratchFlt.size() < samples) + scratchFlt.resize(samples); + + size_t oDone = soxr_output(m_src, scratchFlt.data(), frames); + + m_matrix.mixStereoSampleData(mixInfo, scratchFlt.data(), buf, oDone); + return oDone; +} + +void AudioVoiceStereo::setDefaultMatrixCoefficients() +{ + m_matrix.setDefaultMatrixCoefficients(m_parent.mixInfo().m_channels); +} + +void AudioVoiceStereo::setMonoMatrixCoefficients(const float coefs[8]) +{ + float newCoefs[8][2] = + { + {coefs[0], coefs[0]}, + {coefs[1], coefs[1]}, + {coefs[2], coefs[2]}, + {coefs[3], coefs[3]}, + {coefs[4], coefs[4]}, + {coefs[5], coefs[5]}, + {coefs[6], coefs[6]}, + {coefs[7], coefs[7]} + }; + m_matrix.setMatrixCoefficients(newCoefs); +} + +void AudioVoiceStereo::setStereoMatrixCoefficients(const float coefs[8][2]) +{ + m_matrix.setMatrixCoefficients(coefs); +} + +} diff --git a/lib/audiodev/AudioVoice.hpp b/lib/audiodev/AudioVoice.hpp index c2b56d0..0d4c634 100644 --- a/lib/audiodev/AudioVoice.hpp +++ b/lib/audiodev/AudioVoice.hpp @@ -2,13 +2,90 @@ #define BOO_AUDIOVOICE_HPP #include +#include +#include "boo/audiodev/IAudioVoice.hpp" +#include "AudioMatrix.hpp" namespace boo { +class BaseAudioVoiceEngine; +struct AudioVoiceEngineMixInfo; -class AudioVoice +class AudioVoice : public IAudioVoice { - soxr_t m_voice; + friend class BaseAudioVoiceEngine; + +protected: + /* Mixer-engine relationships */ + BaseAudioVoiceEngine& m_parent; + std::list::iterator m_parentIt; + bool m_bound = false; + void bindVoice(std::list::iterator pIt) + { + m_bound = true; + m_parentIt = pIt; + } + + /* Callback (audio source) */ + IAudioVoiceCallback* m_cb; + + /* Sample-rate converter */ + soxr_t m_src; + bool m_dynamicRate; + + /* Running bool */ + bool m_running = false; + + virtual size_t pumpAndMix(const AudioVoiceEngineMixInfo& mixInfo, size_t frames, int16_t* buf)=0; + virtual size_t pumpAndMix(const AudioVoiceEngineMixInfo& mixInfo, size_t frames, int32_t* buf)=0; + virtual size_t pumpAndMix(const AudioVoiceEngineMixInfo& mixInfo, size_t frames, float* buf)=0; + AudioVoice(BaseAudioVoiceEngine& parent, IAudioVoiceCallback* cb, bool dynamicRate); + +public: + ~AudioVoice(); + + void setPitchRatio(double ratio); + void start(); + void stop(); + void unbindVoice(); +}; + +class AudioVoiceMono : public AudioVoice +{ + AudioMatrixMono m_matrix; + + static size_t SRCCallback(AudioVoiceMono* ctx, + int16_t** data, size_t requestedLen); + + size_t pumpAndMix(const AudioVoiceEngineMixInfo& mixInfo, size_t frames, int16_t* buf); + size_t pumpAndMix(const AudioVoiceEngineMixInfo& mixInfo, size_t frames, int32_t* buf); + size_t pumpAndMix(const AudioVoiceEngineMixInfo& mixInfo, size_t frames, float* buf); + +public: + AudioVoiceMono(BaseAudioVoiceEngine& parent, IAudioVoiceCallback* cb, + double sampleRate, bool dynamicRate); + void setDefaultMatrixCoefficients(); + void setMonoMatrixCoefficients(const float coefs[8]); + void setStereoMatrixCoefficients(const float coefs[8][2]); +}; + +class AudioVoiceStereo : public AudioVoice +{ + AudioMatrixStereo m_matrix; + + static size_t SRCCallback(AudioVoiceStereo* ctx, + int16_t** data, size_t requestedLen); + + size_t pumpAndMix(const AudioVoiceEngineMixInfo& mixInfo, size_t frames, int16_t* buf); + size_t pumpAndMix(const AudioVoiceEngineMixInfo& mixInfo, size_t frames, int32_t* buf); + size_t pumpAndMix(const AudioVoiceEngineMixInfo& mixInfo, size_t frames, float* buf); + +public: + AudioVoiceStereo(BaseAudioVoiceEngine& parent, IAudioVoiceCallback* cb, + double sampleRate, bool dynamicRate); + void setDefaultMatrixCoefficients(); + void setMonoMatrixCoefficients(const float coefs[8]); + void setStereoMatrixCoefficients(const float coefs[8][2]); }; } diff --git a/lib/audiodev/AudioVoiceEngine.cpp b/lib/audiodev/AudioVoiceEngine.cpp new file mode 100644 index 0000000..ebeb762 --- /dev/null +++ b/lib/audiodev/AudioVoiceEngine.cpp @@ -0,0 +1,61 @@ +#include "AudioVoiceEngine.hpp" +#include + +namespace boo +{ + +void BaseAudioVoiceEngine::_pumpAndMixVoices(size_t frames, int16_t* dataOut) +{ + memset(dataOut, 0, sizeof(int16_t) * frames * m_mixInfo.m_channelMap.m_channelCount); + for (AudioVoice* vox : m_activeVoices) + if (vox->m_running) + vox->pumpAndMix(m_mixInfo, frames, dataOut); +} + +void BaseAudioVoiceEngine::_pumpAndMixVoices(size_t frames, int32_t* dataOut) +{ + memset(dataOut, 0, sizeof(int32_t) * frames * m_mixInfo.m_channelMap.m_channelCount); + for (AudioVoice* vox : m_activeVoices) + if (vox->m_running) + vox->pumpAndMix(m_mixInfo, frames, dataOut); +} + +void BaseAudioVoiceEngine::_pumpAndMixVoices(size_t frames, float* dataOut) +{ + memset(dataOut, 0, sizeof(float) * frames * m_mixInfo.m_channelMap.m_channelCount); + for (AudioVoice* vox : m_activeVoices) + if (vox->m_running) + vox->pumpAndMix(m_mixInfo, frames, dataOut); +} + +BaseAudioVoiceEngine::BaseAudioVoiceEngine + (const std::function& getEngineMixInfo) +{ + m_mixInfo = getEngineMixInfo(); +} + +std::unique_ptr +BaseAudioVoiceEngine::allocateNewMonoVoice(double sampleRate, + IAudioVoiceCallback* cb, + bool dynamicPitch) +{ + std::unique_ptr ret = + std::make_unique(*this, cb, sampleRate, dynamicPitch); + AudioVoiceMono* retMono = static_cast(ret.get()); + retMono->bindVoice(m_activeVoices.insert(m_activeVoices.end(), retMono)); + return ret; +} + +std::unique_ptr +BaseAudioVoiceEngine::allocateNewStereoVoice(double sampleRate, + IAudioVoiceCallback* cb, + bool dynamicPitch) +{ + std::unique_ptr ret = + std::make_unique(*this, cb, sampleRate, dynamicPitch); + AudioVoiceStereo* retStereo = static_cast(ret.get()); + retStereo->bindVoice(m_activeVoices.insert(m_activeVoices.end(), retStereo)); + return ret; +} + +} diff --git a/lib/audiodev/AudioVoiceEngine.hpp b/lib/audiodev/AudioVoiceEngine.hpp new file mode 100644 index 0000000..57430a1 --- /dev/null +++ b/lib/audiodev/AudioVoiceEngine.hpp @@ -0,0 +1,51 @@ +#ifndef BOO_AUDIOVOICEENGINE_HPP +#define BOO_AUDIOVOICEENGINE_HPP + +#include "boo/audiodev/IAudioVoiceEngine.hpp" +#include "AudioVoice.hpp" + +namespace boo +{ + +/** Pertinent information from audio backend about optimal mixed-audio representation */ +struct AudioVoiceEngineMixInfo +{ + double m_sampleRate; + soxr_datatype_t m_sampleFormat; + unsigned m_bitsPerSample; + AudioChannelSet m_channels; + ChannelMap m_channelMap; + size_t m_periodFrames; +}; + +/** Base class for managing mixing and sample-rate-conversion amongst active voices */ +class BaseAudioVoiceEngine : public IAudioVoiceEngine +{ +protected: + friend class AudioVoice; + AudioVoiceEngineMixInfo m_mixInfo; + std::list m_activeVoices; + + void _pumpAndMixVoices(size_t frames, int16_t* dataOut); + void _pumpAndMixVoices(size_t frames, int32_t* dataOut); + void _pumpAndMixVoices(size_t frames, float* dataOut); + +public: + BaseAudioVoiceEngine(const std::function& getEngineMixInfo); + + std::unique_ptr allocateNewMonoVoice(double sampleRate, + IAudioVoiceCallback* cb, + bool dynamicPitch=false); + + std::unique_ptr allocateNewStereoVoice(double sampleRate, + IAudioVoiceCallback* cb, + bool dynamicPitch=false); + + const AudioVoiceEngineMixInfo& mixInfo() const {return m_mixInfo;} + AudioChannelSet getAvailableSet() {return m_mixInfo.m_channels;} + void pumpAndMixVoices() {} +}; + +} + +#endif // BOO_AUDIOVOICEENGINE_HPP