diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f15964..adc0881 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -195,6 +195,7 @@ add_library(boo lib/inputdev/GenericPad.cpp include/boo/inputdev/GenericPad.hpp lib/inputdev/DeviceSignature.cpp include/boo/inputdev/DeviceSignature.hpp lib/inputdev/IHIDDevice.hpp + lib/audiodev/WAVOut.cpp lib/audiodev/AudioMatrix.hpp #lib/audiodev/AudioMatrix.cpp lib/audiodev/AudioMatrixSSE.cpp diff --git a/include/boo/audiodev/IAudioVoiceEngine.hpp b/include/boo/audiodev/IAudioVoiceEngine.hpp index e38b922..4cc75d4 100644 --- a/include/boo/audiodev/IAudioVoiceEngine.hpp +++ b/include/boo/audiodev/IAudioVoiceEngine.hpp @@ -64,14 +64,23 @@ struct IAudioVoiceEngine /** Open named MIDI in/out port, name format depends on OS */ virtual std::unique_ptr newRealMIDIInOut(const char* name, ReceiveFunctor&& receiver)=0; - + /** If this returns true, MIDI callbacks are assumed to be *not* thread-safe; need protection via mutex */ virtual bool useMIDILock() const=0; + + /** Get canonical count of frames for each 5ms output block */ + virtual size_t get5MsFrames() const=0; }; /** Construct host platform's voice engine */ std::unique_ptr NewAudioVoiceEngine(); +/** Construct WAV-rendering voice engine */ +std::unique_ptr NewWAVAudioVoiceEngine(const char* path, double sampleRate); +#if _WIN32 +std::unique_ptr NewWAVAudioVoiceEngine(const wchar_t* path, double sampleRate); +#endif + } #endif // BOO_IAUDIOVOICEENGINE_HPP diff --git a/lib/audiodev/AudioSubmix.hpp b/lib/audiodev/AudioSubmix.hpp index cdc3c7a..e263e50 100644 --- a/lib/audiodev/AudioSubmix.hpp +++ b/lib/audiodev/AudioSubmix.hpp @@ -8,6 +8,7 @@ struct AudioUnitVoiceEngine; struct VSTVoiceEngine; +struct WAVOutVoiceEngine; namespace boo { @@ -20,6 +21,7 @@ class AudioSubmix : public IAudioSubmix, public IAudioMix friend struct WASAPIAudioVoiceEngine; friend struct ::AudioUnitVoiceEngine; friend struct ::VSTVoiceEngine; + friend struct ::WAVOutVoiceEngine; /* Mixer-engine relationships */ BaseAudioVoiceEngine& m_root; diff --git a/lib/audiodev/AudioVoice.cpp b/lib/audiodev/AudioVoice.cpp index 3c9852e..98b081c 100644 --- a/lib/audiodev/AudioVoice.cpp +++ b/lib/audiodev/AudioVoice.cpp @@ -88,7 +88,7 @@ void AudioVoiceMono::_resetSampleRate(double sampleRate) soxr_quality_spec_t qSpec = soxr_quality_spec(SOXR_20_BITQ, m_dynamicRate ? SOXR_VR : 0); soxr_error_t err; - m_src = soxr_create(sampleRate, rateOut, 1, + m_src = soxr_create(1 << 1, 1, 1, &err, &ioSpec, &qSpec, nullptr); if (err) diff --git a/lib/audiodev/AudioVoice.hpp b/lib/audiodev/AudioVoice.hpp index 16a5f8b..ce10197 100644 --- a/lib/audiodev/AudioVoice.hpp +++ b/lib/audiodev/AudioVoice.hpp @@ -8,6 +8,7 @@ struct AudioUnitVoiceEngine; struct VSTVoiceEngine; +struct WAVOutVoiceEngine; namespace boo { @@ -22,6 +23,7 @@ class AudioVoice : public IAudioVoice friend struct WASAPIAudioVoiceEngine; friend struct ::AudioUnitVoiceEngine; friend struct ::VSTVoiceEngine; + friend struct ::WAVOutVoiceEngine; protected: /* Mixer-engine relationships */ diff --git a/lib/audiodev/AudioVoiceEngine.hpp b/lib/audiodev/AudioVoiceEngine.hpp index f5f3d11..af7687f 100644 --- a/lib/audiodev/AudioVoiceEngine.hpp +++ b/lib/audiodev/AudioVoiceEngine.hpp @@ -65,6 +65,7 @@ public: const AudioVoiceEngineMixInfo& mixInfo() const; AudioChannelSet getAvailableSet() {return m_mixInfo.m_channels;} void pumpAndMixVoices() {} + size_t get5MsFrames() const {return m_5msFrames;} }; } diff --git a/lib/audiodev/WAVOut.cpp b/lib/audiodev/WAVOut.cpp new file mode 100644 index 0000000..f877003 --- /dev/null +++ b/lib/audiodev/WAVOut.cpp @@ -0,0 +1,200 @@ +#include "AudioVoiceEngine.hpp" +#include "logvisor/logvisor.hpp" +#include "boo/audiodev/IAudioVoiceEngine.hpp" + +static logvisor::Module Log("boo::WAVOut"); + +struct WAVOutVoiceEngine : boo::BaseAudioVoiceEngine +{ + std::vector m_interleavedBuf; + + boo::AudioChannelSet _getAvailableSet() + { + return boo::AudioChannelSet::Stereo; + } + + std::vector> enumerateMIDIDevices() const + { + return {}; + } + + boo::ReceiveFunctor* m_midiReceiver = nullptr; + + struct MIDIIn : public boo::IMIDIIn + { + MIDIIn(bool virt, boo::ReceiveFunctor&& receiver) + : IMIDIIn(virt, std::move(receiver)) {} + + std::string description() const + { + return "WAVOut MIDI"; + } + }; + + std::unique_ptr newVirtualMIDIIn(boo::ReceiveFunctor&& receiver) + { + std::unique_ptr ret = std::make_unique(true, std::move(receiver)); + m_midiReceiver = &ret->m_receiver; + return ret; + } + + std::unique_ptr newVirtualMIDIOut() + { + return {}; + } + + std::unique_ptr newVirtualMIDIInOut(boo::ReceiveFunctor&& receiver) + { + return {}; + } + + std::unique_ptr newRealMIDIIn(const char* name, boo::ReceiveFunctor&& receiver) + { + return {}; + } + + std::unique_ptr newRealMIDIOut(const char* name) + { + return {}; + } + + std::unique_ptr newRealMIDIInOut(const char* name, boo::ReceiveFunctor&& receiver) + { + return {}; + } + + bool useMIDILock() const {return false;} + + FILE* m_fp = nullptr; + size_t m_bytesWritten = 0; + + void prepareWAV(double sampleRate) + { + fwrite("RIFF", 1, 4, m_fp); + uint32_t dataSize = 0; + uint32_t chunkSize = 36 + dataSize; + fwrite(&chunkSize, 1, 4, m_fp); + fwrite("WAVE", 1, 4, m_fp); + + fwrite("fmt ", 1, 4, m_fp); + uint32_t sixteen = 16; + fwrite(&sixteen, 1, 4, m_fp); + uint16_t audioFmt = 3; + fwrite(&audioFmt, 1, 2, m_fp); + uint16_t chCount = 2; + fwrite(&chCount, 1, 2, m_fp); + uint32_t sampRate = sampleRate; + fwrite(&sampRate, 1, 4, m_fp); + uint16_t blockAlign = 8; + uint32_t byteRate = sampRate * blockAlign; + fwrite(&byteRate, 1, 4, m_fp); + fwrite(&blockAlign, 1, 2, m_fp); + uint16_t bps = 32; + fwrite(&bps, 1, 2, m_fp); + + fwrite("data", 1, 4, m_fp); + fwrite(&dataSize, 1, 4, m_fp); + + m_mixInfo.m_periodFrames = 512; + m_mixInfo.m_sampleRate = sampleRate; + m_mixInfo.m_sampleFormat = SOXR_FLOAT32_I; + m_mixInfo.m_bitsPerSample = 32; + _buildAudioRenderClient(); + } + + WAVOutVoiceEngine(const char* path, double sampleRate) + { + m_fp = fopen(path, "wb"); + if (!m_fp) + return; + prepareWAV(sampleRate); + } + +#if _WIN32 + WAVOutVoiceEngine(const wchar_t* path, double sampleRate) + { + m_fp = _wfopen(path, "wb"); + if (!m_fp) + return; + prepareWAV(sampleRate); + } +#endif + + void finishWav() + { + uint32_t dataSize = m_bytesWritten; + + fseek(m_fp, 4, SEEK_SET); + uint32_t chunkSize = 36 + dataSize; + fwrite(&chunkSize, 1, 4, m_fp); + + fseek(m_fp, 40, SEEK_SET); + fwrite(&dataSize, 1, 4, m_fp); + + fclose(m_fp); + } + + ~WAVOutVoiceEngine() + { + finishWav(); + } + + void _buildAudioRenderClient() + { + m_mixInfo.m_channels = _getAvailableSet(); + unsigned chCount = ChannelCount(m_mixInfo.m_channels); + + m_5msFrames = m_mixInfo.m_sampleRate * 5 / 1000; + m_interleavedBuf.resize(2 * m_5msFrames); + + boo::ChannelMap& chMapOut = m_mixInfo.m_channelMap; + chMapOut.m_channelCount = 2; + chMapOut.m_channels[0] = boo::AudioChannel::FrontLeft; + chMapOut.m_channels[1] = boo::AudioChannel::FrontRight; + + while (chMapOut.m_channelCount < chCount) + chMapOut.m_channels[chMapOut.m_channelCount++] = boo::AudioChannel::Unknown; + } + + void _rebuildAudioRenderClient(double sampleRate, size_t periodFrames) + { + m_mixInfo.m_periodFrames = periodFrames; + m_mixInfo.m_sampleRate = sampleRate; + _buildAudioRenderClient(); + + for (boo::AudioVoice* vox : m_activeVoices) + vox->_resetSampleRate(vox->m_sampleRateIn); + for (boo::AudioSubmix* smx : m_activeSubmixes) + smx->_resetOutputSampleRate(); + } + + void pumpAndMixVoices() + { + _pumpAndMixVoices(m_5msFrames, m_interleavedBuf.data()); + fwrite(m_interleavedBuf.data(), 1, m_5msFrames * 8, m_fp); + m_bytesWritten += m_5msFrames * 8; + } +}; + +namespace boo +{ + +std::unique_ptr NewWAVAudioVoiceEngine(const char* path, double sampleRate) +{ + std::unique_ptr ret = std::make_unique(path, sampleRate); + if (!static_cast(*ret).m_fp) + return {}; + return ret; +} + +#if _WIN32 +std::unique_ptr NewWAVAudioVoiceEngine(const wchar_t* path, double sampleRate) +{ + std::unique_ptr ret = std::make_unique(path, sampleRate); + if (!static_cast(*ret).m_fp) + return {}; + return ret; +} +#endif + +}