olc::CodeJam 2022 Entry #olcCodeJam2022
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Seasons-Of-Loneliness/soundwaveEngine.h

1963 lines
50 KiB

/*
+-------------------------------------------------------------+
| OneLoneCoder Sound Wave Engine v0.02 |
| "You wanted noise? Well is this loud enough?" - javidx9 |
+-------------------------------------------------------------+
What is this?
~~~~~~~~~~~~~
olc::SoundWaveEngine is a single file, cross platform audio
interface for lightweight applications that just need a bit of
easy audio manipulation.
It's origins started in the olcNoiseMaker file that accompanied
javidx9's "Code-It-Yourself: Synthesizer" series. It was refactored
and absorbed into the "olcConsoleGameEngine.h" file, and then
refactored again into olcPGEX_Sound.h, that was an extension to
the awesome "olcPixelGameEngine.h" file.
Alas, it went underused and began to rot, with many myths circulating
that "it doesnt work" and "it shouldn't be used". These untruths
made javidx9 feel sorry for the poor file, and he decided to breathe
some new life into it, in anticipation of new videos!
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2018 - 2022 OneLoneCoder.com
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met :
1. Redistributions or derivations of source code must retain the above
copyright notice, this list of conditionsand the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce
the above copyright notice.This list of conditions and the following
disclaimer must be reproduced in the documentation and /or other
materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED.IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES(INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Links
~~~~~
YouTube: https://www.youtube.com/javidx9
Discord: https://discord.gg/WhwHUMV
Twitter: https://www.twitter.com/javidx9
Twitch: https://www.twitch.tv/javidx9
GitHub: https://www.github.com/onelonecoder
Homepage: https://www.onelonecoder.com
Patreon: https://www.patreon.com/javidx9
Thanks
~~~~~~
Gorbit99, Dragoneye, Puol
Authors
~~~~~~~
slavka, MaGetzUb, cstd, Moros1138 & javidx9
(c)OneLoneCoder 2019, 2020, 2021, 2022
*/
/*
Using & Installing On Microsoft Windows
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Microsoft Visual Studio
~~~~~~~~~~~~~~~~~~~~~~~
1) Include the header file "olcSoundWaveEngine.h" from a .cpp file in your project.
2) That's it!
Code::Blocks
~~~~~~~~~~~~
1) Make sure your compiler toolchain is NOT the default one installed with Code::Blocks. That
one is old, out of date, and a general mess. Instead, use MSYS2 to install a recent and
decent GCC toolchain, then configure Code::Blocks to use it
Guide for installing recent GCC for Windows:
https://www.msys2.org/
Guide for configuring code::blocks:
https://solarianprogrammer.com/2019/11/05/install-gcc-windows/
https://solarianprogrammer.com/2019/11/16/install-codeblocks-gcc-windows-build-c-cpp-fortran-programs/
2) Include the header file "olcSoundWaveEngine.h" from a .cpp file in your project.
3) Add these libraries to "Linker Options": user32 winmm
4) Set this "Compiler Option": -std=c++17
*/
/*
Using & Installing On Linux
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
GNU Compiler Collection (GCC)
~~~~~~~~~~~~~~~~~~~~~~~
1) Include the header file "olcSoundWaveEngine.h" from a .cpp file in your project.
2) Build with the following command:
g++ olcSoundWaveEngineExample.cpp -o olcSoundWaveEngineExample -lpulse -lpulse-simple -std=c++17
3) That's it!
*/
/*
Using in multiple-file projects
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you intend to use olcSoundWaveEngine across multiple files, it's important to only have one
instance of the implementation. This is done using the compiler preprocessor definition: OLC_SOUNDWAVE
This is defined typically before the header is included in teh translation unit you wish the implementation
to be associated with. To avoid things getting messy I recommend you create a file "olcSoundWaveEngine.cpp"
and that file includes ONLY the following code:
#define OLC_SOUNDWAVE
#include "olcSoundWaveEngine.h"
*/
/*
0.01: olcPGEX_Sound.h reworked
+Changed timekeeping to double, added accuracy fix - Thanks scripticuk
+Concept of audio drivers and interface
+All internal timing now double precision
+All internal sampling now single precsion
+Loading form WAV files
+LERPed sampling from all buffers
+Multi-channel audio support
0.02: +Support multi-channel wave files
+Support for 24-bit wave files
+Wave files are now sample rate invariant
+Linux PulseAudio Updated
+Linux ALSA Updated
+WinMM Updated
+CMake Compatibility
=Fix wave format durations preventing playback
=Various bug fixes
*/
#pragma once
#ifndef OLC_SOUNDWAVE_H
#define OLC_SOUNDWAVE_H
#include <cmath>
#include <cstdint>
#include <cstring>
#include <vector>
#include <list>
#include <atomic>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <string>
#include <algorithm>
#include <fstream>
#include <iostream>
// Compiler/System Sensitivity
#if !defined(SOUNDWAVE_USING_WINMM) && !defined(SOUNDWAVE_USING_WASAPI) && \
!defined(SOUNDWAVE_USING_XAUDIO) && !defined(SOUNDWAVE_USING_OPENAL) && \
!defined(SOUNDWAVE_USING_ALSA) && !defined(SOUNDWAVE_USING_SDLMIXER) && \
!defined(SOUNDWAVE_USING_PULSE) \
#if defined(_WIN32)
#define SOUNDWAVE_USING_WINMM
#endif
#if defined(__linux__)
#define SOUNDWAVE_USING_PULSE
#endif
#if defined(__APPLE__)
#define SOUNDWAVE_USING_SDLMIXER
#endif
#if defined(__EMSCRIPTEN__)
#define SOUNDWAVE_USING_SDLMIXER
#endif
#endif
namespace olc::sound
{
namespace wave
{
// Physically represents a .WAV file, but the data is stored
// as normalised floating point values
template<class T = float>
class File
{
public:
File() = default;
File(const size_t nChannels, const size_t nSampleSize, const size_t nSampleRate, const size_t nSamples)
{
m_nChannels = nChannels;
m_nSampleSize = nSampleSize;
m_nSamples = nSamples;
m_nSampleRate = nSampleRate;
m_dDuration = double(m_nSamples) / double(m_nSampleRate);
m_dDurationInSamples = double(m_nSamples);
m_pRawData = std::make_unique<T[]>(m_nSamples * m_nChannels);
}
public:
T* data() const
{
return m_pRawData.get();
}
size_t samples() const
{
return m_nSamples;
}
size_t channels() const
{
return m_nChannels;
}
size_t samplesize() const
{
return m_nSampleSize;
}
size_t samplerate() const
{
return m_nSampleRate;
}
double duration() const
{
return m_dDuration;
}
double durationInSamples() const
{
return m_dDurationInSamples;
}
bool LoadFile(const std::string& sFilename)
{
std::ifstream ifs(sFilename, std::ios::binary);
if (!ifs.is_open())
return false;
struct WaveFormatHeader
{
uint16_t wFormatTag; /* format type */
uint16_t nChannels; /* number of channels (i.e. mono, stereo...) */
uint32_t nSamplesPerSec; /* sample rate */
uint32_t nAvgBytesPerSec; /* for buffer estimation */
uint16_t nBlockAlign; /* block size of data */
uint16_t wBitsPerSample; /* number of bits per sample of mono data */
};
WaveFormatHeader header{ 0 };
m_pRawData.reset();
char dump[4];
ifs.read(dump, sizeof(uint8_t) * 4); // Read "RIFF"
if (strncmp(dump, "RIFF", 4) != 0) return false;
ifs.read(dump, sizeof(uint8_t) * 4); // Not Interested
ifs.read(dump, sizeof(uint8_t) * 4); // Read "WAVE"
if (strncmp(dump, "WAVE", 4) != 0) return false;
// Read Wave description chunk
ifs.read(dump, sizeof(uint8_t) * 4); // Read "fmt "
ifs.read(dump, sizeof(uint8_t) * 4); // Not Interested
ifs.read((char*)&header, sizeof(WaveFormatHeader)); // Read Wave Format Structure chunk
// Search for audio data chunk
int32_t nChunksize = 0;
ifs.read(dump, sizeof(uint8_t) * 4); // Read chunk header
ifs.read((char*)&nChunksize, sizeof(uint32_t)); // Read chunk size
while (strncmp(dump, "data", 4) != 0)
{
// Not audio data, so just skip it
ifs.seekg(nChunksize, std::ios::cur);
ifs.read(dump, sizeof(uint8_t) * 4); // Read next chunk header
ifs.read((char*)&nChunksize, sizeof(uint32_t)); // Read next chunk size
}
// Finally got to data, so read it all in and convert to float samples
m_nSampleSize = header.wBitsPerSample >> 3;
m_nSamples = nChunksize / (header.nChannels * m_nSampleSize);
m_nChannels = header.nChannels;
m_nSampleRate = header.nSamplesPerSec;
m_pRawData = std::make_unique<T[]>(m_nSamples * m_nChannels);
m_dDuration = double(m_nSamples) / double(m_nSampleRate);
m_dDurationInSamples = double(m_nSamples);
T* pSample = m_pRawData.get();
// Read in audio data and normalise
for (long i = 0; i < m_nSamples; i++)
{
for (int c = 0; c < m_nChannels; c++)
{
switch (m_nSampleSize)
{
case 1:
{
int8_t s = 0;
ifs.read((char*)&s, sizeof(int8_t));
*pSample = T(s) / T(std::numeric_limits<int8_t>::max());
}
break;
case 2:
{
int16_t s = 0;
ifs.read((char*)&s, sizeof(int16_t));
*pSample = T(s) / T(std::numeric_limits<int16_t>::max());
}
break;
case 3: // 24-bit
{
int32_t s = 0;
ifs.read((char*)&s, 3);
if (s & (1 << 23)) s |= 0xFF000000;
*pSample = T(s) / T(std::pow(2, 23)-1);
}
break;
case 4:
{
int32_t s = 0;
ifs.read((char*)&s, sizeof(int32_t));
*pSample = T(s) / T(std::numeric_limits<int32_t>::max());
}
break;
}
pSample++;
}
}
return true;
}
bool SaveFile(const std::string& sFilename)
{
return false;
}
protected:
std::unique_ptr<T[]> m_pRawData;
size_t m_nSamples = 0;
size_t m_nChannels = 0;
size_t m_nSampleRate = 0;
size_t m_nSampleSize = 0;
double m_dDuration = 0.0;
double m_dDurationInSamples = 0.0;
};
template<typename T>
class View
{
public:
View() = default;
View(const T* pData, const size_t nSamples)
{
SetData(pData, nSamples);
}
public:
void SetData(T const* pData, const size_t nSamples, const size_t nStride = 1, const size_t nOffset = 0)
{
m_pData = pData;
m_nSamples = nSamples;
m_nStride = nStride;
m_nOffset = nOffset;
}
double GetSample(const double dSample) const
{
double d1 = std::floor(dSample);
size_t p1 = static_cast<size_t>(d1);
size_t p2 = p1 + 1;
double t = dSample - d1;
double a = GetValue(p1);
double b = GetValue(p2);
return a + t * (b - a); // std::lerp in C++20
}
std::pair<double, double> GetRange(const double dSample1, const double dSample2) const
{
if (dSample1 < 0 || dSample2 < 0)
return { 0,0 };
if (dSample1 > m_nSamples && dSample2 > m_nSamples)
return { 0,0 };
double dMin, dMax;
double d = GetSample(dSample1);
dMin = dMax = d;
size_t n1 = static_cast<size_t>(std::ceil(dSample1));
size_t n2 = static_cast<size_t>(std::floor(dSample2));
for (size_t n = n1; n < n2; n++)
{
d = GetValue(n);
dMin = std::min(dMin, d);
dMax = std::max(dMax, d);
}
d = GetSample(dSample2);
dMin = std::min(dMin, d);
dMax = std::max(dMax, d);
return { dMin, dMax };
}
T GetValue(const size_t nSample) const
{
if (nSample >= m_nSamples)
return 0;
else
return m_pData[m_nOffset + nSample * m_nStride];
}
private:
const T* m_pData = nullptr;
size_t m_nSamples = 0;
size_t m_nStride = 1;
size_t m_nOffset = 0;
};
}
template<typename T = float>
class Wave_generic
{
public:
Wave_generic() = default;
Wave_generic(std::string sWavFile) { LoadAudioWaveform(sWavFile); }
Wave_generic(std::istream& sStream) { LoadAudioWaveform(sStream); }
Wave_generic(const char* pData, const size_t nBytes) { LoadAudioWaveform(pData, nBytes); }
Wave_generic(const size_t nChannels, const size_t nSampleSize, const size_t nSampleRate, const size_t nSamples)
{
vChannelView.clear();
file = wave::File<T>(nChannels, nSampleSize, nSampleRate, nSamples);
vChannelView.resize(file.channels());
for (uint32_t c = 0; c < file.channels(); c++)
vChannelView[c].SetData(file.data(), file.samples(), file.channels(), c);
}
bool LoadAudioWaveform(std::string sWavFile)
{
vChannelView.clear();
if (file.LoadFile(sWavFile))
{
// Setup views for each channel
vChannelView.resize(file.channels());
for (uint32_t c = 0; c < file.channels(); c++)
vChannelView[c].SetData(file.data(), file.samples(), file.channels(), c);
return true;
}
return false;
}
bool LoadAudioWaveform(std::istream& sStream) { return false; }
bool LoadAudioWaveform(const char* pData, const size_t nBytes) { return false; }
std::vector<wave::View<T>> vChannelView;
wave::File<T> file;
};
typedef Wave_generic<float> Wave;
struct WaveInstance
{
Wave* pWave = nullptr;
double dInstanceTime = 0.0;
double dDuration = 0.0;
double dSpeedModifier = 1.0;
bool bFinished = false;
bool bLoop = false;
bool bFlagForStop = false;
};
typedef std::list<WaveInstance>::iterator PlayingWave;
namespace driver
{
class Base;
}
// Container class for Basic Sound Manipulation
class WaveEngine
{
public:
WaveEngine();
virtual ~WaveEngine();
// Configure Audio Hardware
bool InitialiseAudio(uint32_t nSampleRate = 44100, uint32_t nChannels = 1, uint32_t nBlocks = 8, uint32_t nBlockSamples = 512);
// Release Audio Hardware
bool DestroyAudio();
// Call to get the names of all the devices capable of audio output - DACs. An entry
// from the returned collection can be specified as the device to use in UseOutputDevice()
std::vector<std::string> GetOutputDevices();
// Specify a device for audio output prior to calling InitialiseAudio()
void UseOutputDevice(const std::string& sDeviceOut);
// Call to get the names of all the devices capable of audio input - ADCs. An entry
// from the returned collection can be specified as the device to use in UseInputDevice()
std::vector<std::string> GetInputDevices();
// Specify a device for audio input prior to calling InitialiseAudio()
void UseInputDevice(const std::string& sDeviceOut);
void SetCallBack_NewSample(std::function<void(double)> func);
void SetCallBack_SynthFunction(std::function<float(uint32_t, double)> func);
void SetCallBack_FilterFunction(std::function<float(uint32_t, double, float)> func);
public:
void SetOutputVolume(const float fVolume);
PlayingWave PlayWaveform(Wave* pWave, bool bLoop = false, double dSpeed = 1.0);
void StopWaveform(const PlayingWave& w);
void StopAll();
private:
uint32_t FillOutputBuffer(std::vector<float>& vBuffer, const uint32_t nBufferOffset, const uint32_t nRequiredSamples);
private:
std::unique_ptr<driver::Base> m_driver;
std::function<void(double)> m_funcNewSample;
std::function<float(uint32_t, double)> m_funcUserSynth;
std::function<float(uint32_t, double, float)> m_funcUserFilter;
private:
uint32_t m_nSampleRate = 44100;
uint32_t m_nChannels = 1;
uint32_t m_nBlocks = 8;
uint32_t m_nBlockSamples = 512;
double m_dSamplePerTime = 44100.0;
double m_dTimePerSample = 1.0 / 44100;
double m_dGlobalTime = 0.0;
float m_fOutputVolume = 1.0;
std::string m_sInputDevice;
std::string m_sOutputDevice;
private:
std::list<WaveInstance> m_listWaves;
public:
uint32_t GetSampleRate() const;
uint32_t GetChannels() const;
uint32_t GetBlocks() const;
uint32_t GetBlockSampleCount() const;
double GetTimePerSample() const;
// Friends, for access to FillOutputBuffer from Drivers
friend class driver::Base;
};
namespace driver
{
// DRIVER DEVELOPERS ONLY!!!
//
// This interface allows SoundWave to exchange data with OS audio systems. It
// is not intended of use by regular users.
class Base
{
public:
Base(WaveEngine* pHost);
virtual ~Base();
public:
// [IMPLEMENT] Opens a connection to the hardware device, returns true if success
virtual bool Open(const std::string& sOutputDevice, const std::string& sInputDevice);
// [IMPLEMENT] Starts a process that repeatedly requests audio, returns true if success
virtual bool Start();
// [IMPLEMENT] Stops a process form requesting audio
virtual void Stop();
// [IMPLEMENT] Closes any connections to hardware devices
virtual void Close();
virtual std::vector<std::string> EnumerateOutputDevices();
virtual std::vector<std::string> EnumerateInputDevices();
protected:
// [IMPLEMENT IF REQUIRED] Called by driver to exchange data with SoundWave System. Your
// implementation will call this function providing a "DAC" buffer to be filled by
// SoundWave from a buffer of floats filled by the user.
void ProcessOutputBlock(std::vector<float>& vFloatBuffer, std::vector<short>& vDACBuffer);
// [IMPLEMENT IF REQUIRED] Called by driver to exchange data with SoundWave System.
void GetFullOutputBlock(std::vector<float>& vFloatBuffer);
// Handle to SoundWave, to interrogate optons, and get user data
WaveEngine* m_pHost = nullptr;
};
}
namespace synth
{
class Property
{
public:
double value = 0.0f;
public:
Property() = default;
Property(double f);
public:
Property& operator =(const double f);
};
class Trigger
{
};
class Module
{
public:
virtual void Update(uint32_t nChannel, double dTime, double dTimeStep) = 0;
};
class ModularSynth
{
public:
ModularSynth();
public:
bool AddModule(Module* pModule);
bool RemoveModule(Module* pModule);
bool AddPatch(Property* pInput, Property* pOutput);
bool RemovePatch(Property* pInput, Property* pOutput);
public:
void UpdatePatches();
void Update(uint32_t nChannel, double dTime, double dTimeStep);
protected:
std::vector<Module*> m_vModules;
std::vector<std::pair<Property*, Property*>> m_vPatches;
};
namespace modules
{
class Oscillator : public Module
{
public:
enum class Type
{
Sine,
Saw,
Square,
Triangle,
PWM,
Wave,
Noise,
};
public:
// Primary frequency of oscillation
Property frequency = 0.0f;
// Primary amplitude of output
Property amplitude = 1.0f;
// LFO input if required
Property lfo_input = 0.0f;
// Primary Output
Property output;
// Tweakable Parameter
Property parameter = 0.0;
Type waveform = Type::Sine;
Wave* pWave = nullptr;
private:
double phase_acc = 0.0f;
double max_frequency = 20000.0;
uint32_t random_seed = 0xB00B1E5;
double rndDouble(double min, double max);
uint32_t rnd();
public:
virtual void Update(uint32_t nChannel, double dTime, double dTimeStep) override;
};
}
}
}
#if defined(SOUNDWAVE_USING_WINMM)
#define _WIN32_LEAN_AND_MEAN
#include <Windows.h>
#undef min
#undef max
namespace olc::sound::driver
{
class WinMM : public Base
{
public:
WinMM(WaveEngine* pHost);
~WinMM();
protected:
bool Open(const std::string& sOutputDevice, const std::string& sInputDevice) override;
bool Start() override;
void Stop() override;
void Close() override;
private:
void DriverLoop();
void FreeAudioBlock();
static void CALLBACK waveOutProc(HWAVEOUT hWaveOut, UINT uMsg, DWORD_PTR dwInstance, DWORD dwParam1, DWORD dwParam2);
HWAVEOUT m_hwDevice = nullptr;
std::thread m_thDriverLoop;
std::atomic<bool> m_bDriverLoopActive{ false };
std::unique_ptr<std::vector<short>[]> m_pvBlockMemory;
std::unique_ptr<WAVEHDR[]> m_pWaveHeaders;
std::atomic<unsigned int> m_nBlockFree = 0;
std::condition_variable m_cvBlockNotZero;
std::mutex m_muxBlockNotZero;
uint32_t m_nBlockCurrent = 0;
};
}
#endif // SOUNDWAVE_USING_WINMM
#if defined(SOUNDWAVE_USING_SDLMIXER)
#if defined(__EMSCRIPTEN__)
#include <SDL2/SDL_mixer.h>
#else
#include <SDL_mixer.h>
#endif
namespace olc::sound::driver
{
class SDLMixer final : public Base
{
public:
explicit SDLMixer(WaveEngine* pHost);
~SDLMixer() final;
protected:
bool Open(const std::string& sOutputDevice, const std::string& sInputDevice) final;
bool Start() final;
void Stop() final;
void Close() final;
private:
void FillChunkBuffer(const std::vector<float>& userData) const;
static void SDLMixerCallback(int channel);
private:
bool m_keepRunning = false;
Uint16 m_haveFormat = AUDIO_F32SYS;
std::vector<Uint8> audioBuffer;
Mix_Chunk audioChunk;
static SDLMixer* instance;
};
}
#endif // SOUNDWAVE_USING_SDLMIXER
#if defined(SOUNDWAVE_USING_ALSA)
#include <alsa/asoundlib.h>
#include <poll.h>
#include <iostream>
namespace olc::sound::driver
{
// Not thread-safe
template<typename T>
class RingBuffer
{
public:
RingBuffer()
{ }
void Resize(unsigned int bufnum = 0, unsigned int buflen = 0)
{
m_vBuffers.resize(bufnum);
for (auto &vBuffer : m_vBuffers)
vBuffer.resize(buflen);
}
std::vector<T>& GetFreeBuffer()
{
assert(!IsFull());
std::vector<T>& result = m_vBuffers[m_nTail];
m_nTail = Next(m_nTail);
return result;
}
std::vector<T>& GetFullBuffer()
{
assert(!IsEmpty());
std::vector<T>& result = m_vBuffers[m_nHead];
m_nHead = Next(m_nHead);
return result;
}
bool IsEmpty()
{
return m_nHead == m_nTail;
}
bool IsFull()
{
return m_nHead == Next(m_nTail);
}
private:
unsigned int Next(unsigned int current)
{
return (current + 1) % m_vBuffers.size();
}
std::vector<std::vector<T>> m_vBuffers;
unsigned int m_nHead = 0;
unsigned int m_nTail = 0;
};
class ALSA : public Base
{
public:
ALSA(WaveEngine* pHost);
~ALSA();
protected:
bool Open(const std::string& sOutputDevice, const std::string& sInputDevice) override;
bool Start() override;
void Stop() override;
void Close() override;
private:
void DriverLoop();
snd_pcm_t *m_pPCM;
RingBuffer<float> m_rBuffers;
std::atomic<bool> m_bDriverLoopActive{ false };
std::thread m_thDriverLoop;
};
}
#endif // SOUNDWAVE_USING_ALSA
#if defined(SOUNDWAVE_USING_PULSE)
#include <pulse/simple.h>
namespace olc::sound::driver
{
class PulseAudio : public Base
{
public:
PulseAudio(WaveEngine* pHost);
~PulseAudio();
protected:
bool Open(const std::string& sOutputDevice, const std::string& sInputDevice) override;
bool Start() override;
void Stop() override;
void Close() override;
private:
void DriverLoop();
pa_simple *m_pPA;
std::atomic<bool> m_bDriverLoopActive{ false };
std::thread m_thDriverLoop;
};
}
#endif // SOUNDWAVE_USING_PULSE
#ifdef OLC_SOUNDWAVE
#undef OLC_SOUNDWAVE
namespace olc::sound
{
WaveEngine::WaveEngine()
{
m_sInputDevice = "NONE";
m_sOutputDevice = "DEFAULT";
#if defined(SOUNDWAVE_USING_WINMM)
m_driver = std::make_unique<driver::WinMM>(this);
#endif
#if defined(SOUNDWAVE_USING_WASAPI)
m_driver = std::make_unique<driver::WASAPI>(this);
#endif
#if defined(SOUNDWAVE_USING_XAUDIO)
m_driver = std::make_unique<driver::XAudio>(this);
#endif
#if defined(SOUNDWAVE_USING_OPENAL)
m_driver = std::make_unique<driver::OpenAL>(this);
#endif
#if defined(SOUNDWAVE_USING_ALSA)
m_driver = std::make_unique<driver::ALSA>(this);
#endif
#if defined(SOUNDWAVE_USING_SDLMIXER)
m_driver = std::make_unique<driver::SDLMixer>(this);
#endif
#if defined(SOUNDWAVE_USING_PULSE)
m_driver = std::make_unique<driver::PulseAudio>(this);
#endif
}
WaveEngine::~WaveEngine()
{
DestroyAudio();
}
std::vector<std::string> WaveEngine::GetOutputDevices()
{
return { "XXX" };
}
void WaveEngine::UseOutputDevice(const std::string& sDeviceOut)
{
m_sOutputDevice = sDeviceOut;
}
std::vector<std::string> WaveEngine::GetInputDevices()
{
return { "XXX" };
}
void WaveEngine::UseInputDevice(const std::string& sDeviceIn)
{
m_sInputDevice = sDeviceIn;
}
bool WaveEngine::InitialiseAudio(uint32_t nSampleRate, uint32_t nChannels, uint32_t nBlocks, uint32_t nBlockSamples)
{
m_nSampleRate = nSampleRate;
m_nChannels = nChannels;
m_nBlocks = nBlocks;
m_nBlockSamples = nBlockSamples;
m_dSamplePerTime = double(nSampleRate);
m_dTimePerSample = 1.0 / double(nSampleRate);
m_driver->Open(m_sOutputDevice, m_sInputDevice);
m_driver->Start();
return false;
}
bool WaveEngine::DestroyAudio()
{
StopAll();
m_driver->Stop();
m_driver->Close();
return false;
}
void WaveEngine::SetCallBack_NewSample(std::function<void(double)> func)
{
m_funcNewSample = func;
}
void WaveEngine::SetCallBack_SynthFunction(std::function<float(uint32_t, double)> func)
{
m_funcUserSynth = func;
}
void WaveEngine::SetCallBack_FilterFunction(std::function<float(uint32_t, double, float)> func)
{
m_funcUserFilter = func;
}
PlayingWave WaveEngine::PlayWaveform(Wave* pWave, bool bLoop, double dSpeed)
{
WaveInstance wi;
wi.bLoop = bLoop;
wi.pWave = pWave;
wi.dSpeedModifier = dSpeed * double(pWave->file.samplerate()) / m_dSamplePerTime;
wi.dDuration = pWave->file.duration() / dSpeed;
wi.dInstanceTime = m_dGlobalTime;
m_listWaves.push_back(wi);
return std::prev(m_listWaves.end());
}
void WaveEngine::StopWaveform(const PlayingWave& w)
{
w->bFlagForStop = true;
}
void WaveEngine::StopAll()
{
for (auto& wave : m_listWaves)
{
wave.bFlagForStop = true;
}
}
void WaveEngine::SetOutputVolume(const float fVolume)
{
m_fOutputVolume = std::clamp(fVolume, 0.0f, 1.0f);
}
uint32_t WaveEngine::FillOutputBuffer(std::vector<float>& vBuffer, const uint32_t nBufferOffset, const uint32_t nRequiredSamples)
{
for (uint32_t nSample = 0; nSample < nRequiredSamples; nSample++)
{
double dSampleTime = m_dGlobalTime + nSample * m_dTimePerSample;
if (m_funcNewSample)
m_funcNewSample(dSampleTime);
for (uint32_t nChannel = 0; nChannel < m_nChannels; nChannel++)
{
// Construct the sample
float fSample = 0.0f;
// 1) Sample any active waves
for (auto& wave : m_listWaves)
{
// Is wave instance flagged for stopping?
if (wave.bFlagForStop)
{
wave.bFinished = true;
}
else
{
// Calculate offset into wave instance
double dTimeOffset = dSampleTime - wave.dInstanceTime;
// If offset is larger than wave then...
if (dTimeOffset >= wave.dDuration)
{
if (wave.bLoop)
{
// ...if looping, reset the wave instance
wave.dInstanceTime = dSampleTime;
}
else
{
// ...if not looping, flag wave instance as dead
wave.bFinished = true;
}
}
else
{
// OR, sample the waveform from the correct channel
fSample += float(wave.pWave->vChannelView[nChannel % wave.pWave->file.channels()].GetSample(dTimeOffset * m_dSamplePerTime * wave.dSpeedModifier));
}
}
}
// Remove waveform instances that have finished
m_listWaves.remove_if([](const WaveInstance& wi) {return wi.bFinished; });
// 2) If user is synthesizing, request sample
if (m_funcUserSynth)
fSample += m_funcUserSynth(nChannel, dSampleTime);
// 3) Apply global filters
// 4) If user is filtering, allow manipulation of output
if (m_funcUserFilter)
fSample = m_funcUserFilter(nChannel, dSampleTime, fSample);
// Place sample in buffer
vBuffer[nBufferOffset + nSample * m_nChannels + nChannel] = fSample * m_fOutputVolume;
}
}
// UPdate global time, accounting for error (thanks scripticuk)
m_dGlobalTime += nRequiredSamples * m_dTimePerSample;
return nRequiredSamples;
}
uint32_t WaveEngine::GetSampleRate() const
{
return m_nSampleRate;
}
uint32_t WaveEngine::GetChannels() const
{
return m_nChannels;
}
uint32_t WaveEngine::GetBlocks() const
{
return m_nBlocks;
}
uint32_t WaveEngine::GetBlockSampleCount() const
{
return m_nBlockSamples;
}
double WaveEngine::GetTimePerSample() const
{
return m_dTimePerSample;
}
namespace driver
{
Base::Base(olc::sound::WaveEngine* pHost) : m_pHost(pHost)
{}
Base::~Base()
{}
bool Base::Open(const std::string& sOutputDevice, const std::string& sInputDevice)
{
return false;
}
bool Base::Start()
{
return false;
}
void Base::Stop()
{
}
void Base::Close()
{
}
std::vector<std::string> Base::EnumerateOutputDevices()
{
return { "DEFAULT" };
}
std::vector<std::string> Base::EnumerateInputDevices()
{
return { "NONE" };
}
void Base::ProcessOutputBlock(std::vector<float>& vFloatBuffer, std::vector<short>& vDACBuffer)
{
constexpr float fMaxSample = float(std::numeric_limits<short>::max());
constexpr float fMinSample = float(std::numeric_limits<short>::min());
// So... why not use vFloatBuffer.size()? Well with this implementation
// we can, but i suspect there may be some platforms that request a
// specific number of samples per "loop" rather than this block architecture
uint32_t nSamplesToProcess = m_pHost->GetBlockSampleCount();
uint32_t nSampleOffset = 0;
while (nSamplesToProcess > 0)
{
uint32_t nSamplesGathered = m_pHost->FillOutputBuffer(vFloatBuffer, nSampleOffset, nSamplesToProcess);
// Vector is in float32 format, so convert to hardware required format
for (uint32_t n = 0; n < nSamplesGathered; n++)
{
for (uint32_t c = 0; c < m_pHost->GetChannels(); c++)
{
size_t nSampleID = nSampleOffset + (n * m_pHost->GetChannels() + c);
vDACBuffer[nSampleID] = short(std::clamp(vFloatBuffer[nSampleID] * fMaxSample, fMinSample, fMaxSample));
}
}
nSampleOffset += nSamplesGathered;
nSamplesToProcess -= nSamplesGathered;
}
}
void Base::GetFullOutputBlock(std::vector<float>& vFloatBuffer)
{
uint32_t nSamplesToProcess = m_pHost->GetBlockSampleCount();
uint32_t nSampleOffset = 0;
while (nSamplesToProcess > 0)
{
uint32_t nSamplesGathered = m_pHost->FillOutputBuffer(vFloatBuffer, nSampleOffset, nSamplesToProcess);
nSampleOffset += nSamplesGathered;
nSamplesToProcess -= nSamplesGathered;
}
}
}
namespace synth
{
Property::Property(double f)
{
value = std::clamp(f, -1.0, 1.0);
}
Property& Property::operator =(const double f)
{
value = std::clamp(f, -1.0, 1.0);
return *this;
}
ModularSynth::ModularSynth()
{
}
bool ModularSynth::AddModule(Module* pModule)
{
// Check if module already added
if (std::find(m_vModules.begin(), m_vModules.end(), pModule) == std::end(m_vModules))
{
m_vModules.push_back(pModule);
return true;
}
return false;
}
bool ModularSynth::RemoveModule(Module* pModule)
{
if (std::find(m_vModules.begin(), m_vModules.end(), pModule) == std::end(m_vModules))
{
m_vModules.erase(std::remove(m_vModules.begin(), m_vModules.end(), pModule), m_vModules.end());
return true;
}
return false;
}
bool ModularSynth::AddPatch(Property* pInput, Property* pOutput)
{
// Does patch exist?
std::pair<Property*, Property*> newPatch = std::pair<Property*, Property*>(pInput, pOutput);
if (std::find(m_vPatches.begin(), m_vPatches.end(), newPatch) == std::end(m_vPatches))
{
// Patch doesnt exist, now check if either are null
if (pInput != nullptr && pOutput != nullptr)
{
m_vPatches.push_back(newPatch);
return true;
}
}
return false;
}
bool ModularSynth::RemovePatch(Property* pInput, Property* pOutput)
{
std::pair<Property*, Property*> newPatch = std::pair<Property*, Property*>(pInput, pOutput);
if (std::find(m_vPatches.begin(), m_vPatches.end(), newPatch) == std::end(m_vPatches))
{
m_vPatches.erase(std::remove(m_vPatches.begin(), m_vPatches.end(), newPatch), m_vPatches.end());
return true;
}
return false;
}
void ModularSynth::UpdatePatches()
{
// Update patches
for (auto& patch : m_vPatches)
{
patch.second->value = patch.first->value;
}
}
void ModularSynth::Update(uint32_t nChannel, double dTime, double dTimeStep)
{
// Now update synth
for (auto& pModule : m_vModules)
{
pModule->Update(nChannel, dTime, dTimeStep);
}
}
namespace modules
{
void Oscillator::Update(uint32_t nChannel, double dTime, double dTimeStep)
{
// We use phase accumulation to combat change in parameter glitches
double w = frequency.value * max_frequency * dTimeStep;
phase_acc += w + lfo_input.value * frequency.value;
if (phase_acc >= 2.0) phase_acc -= 2.0;
switch (waveform)
{
case Type::Sine:
output = amplitude.value * sin(phase_acc * 2.0 * 3.14159);
break;
case Type::Saw:
output = amplitude.value * (phase_acc - 1.0) * 2.0;
break;
case Type::Square:
output = amplitude.value * (phase_acc >= 1.0) ? 1.0 : -1.0;
break;
case Type::Triangle:
output = amplitude.value * (phase_acc < 1.0) ? (phase_acc * 0.5) : (1.0 - phase_acc * 0.5);
break;
case Type::PWM:
output = amplitude.value * (phase_acc >= (parameter.value + 1.0)) ? 1.0 : -1.0;
break;
case Type::Wave:
if(pWave != nullptr)
output = amplitude.value * pWave->vChannelView[nChannel].GetSample(phase_acc * 0.5 * pWave->file.durationInSamples());
break;
case Type::Noise:
output = amplitude.value * rndDouble(-1.0, 1.0);
break;
}
}
double Oscillator::rndDouble(double min, double max)
{
return ((double)rnd() / (double)(0x7FFFFFFF)) * (max - min) + min;
}
uint32_t Oscillator::rnd()
{
random_seed += 0xe120fc15;
uint64_t tmp;
tmp = (uint64_t)random_seed * 0x4a39b70d;
uint32_t m1 = uint32_t(((tmp >> 32) ^ tmp) & 0xFFFFFFFF);
tmp = (uint64_t)m1 * 0x12fad5c9;
uint32_t m2 = uint32_t(((tmp >> 32) ^ tmp) & 0xFFFFFFFF);
return m2;
}
}
}
}
#if defined(SOUNDWAVE_USING_WINMM)
// WinMM Driver Implementation
namespace olc::sound::driver
{
#pragma comment(lib, "winmm.lib")
WinMM::WinMM(WaveEngine* pHost) : Base(pHost)
{ }
WinMM::~WinMM()
{
Stop();
Close();
}
bool WinMM::Open(const std::string& sOutputDevice, const std::string& sInputDevice)
{
// Device is available
WAVEFORMATEX waveFormat;
waveFormat.wFormatTag = WAVE_FORMAT_PCM;
waveFormat.nSamplesPerSec = m_pHost->GetSampleRate();
waveFormat.wBitsPerSample = sizeof(short) * 8;
waveFormat.nChannels = m_pHost->GetChannels();
waveFormat.nBlockAlign = (waveFormat.wBitsPerSample / 8) * waveFormat.nChannels;
waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign;
waveFormat.cbSize = 0;
// Open Device if valid
if (waveOutOpen(&m_hwDevice, WAVE_MAPPER, &waveFormat, (DWORD_PTR)WinMM::waveOutProc, (DWORD_PTR)this, CALLBACK_FUNCTION) != S_OK)
return false;
// Allocate array of wave header objects, one per block
m_pWaveHeaders = std::make_unique<WAVEHDR[]>(m_pHost->GetBlocks());
// Allocate block memory - I dont like vector of vectors, so going with this mess instead
// My std::vector's content will change, but their size never will - they are basically array now
m_pvBlockMemory = std::make_unique<std::vector<short>[]>(m_pHost->GetBlocks());
for (size_t i = 0; i < m_pHost->GetBlocks(); i++)
m_pvBlockMemory[i].resize(m_pHost->GetBlockSampleCount() * m_pHost->GetChannels(), 0);
// Link headers to block memory - clever, so we only move headers about
// rather than memory...
for (unsigned int n = 0; n < m_pHost->GetBlocks(); n++)
{
m_pWaveHeaders[n].dwBufferLength = DWORD(m_pvBlockMemory[0].size() * sizeof(short));
m_pWaveHeaders[n].lpData = (LPSTR)(m_pvBlockMemory[n].data());
}
// To begin with, all blocks are free
m_nBlockFree = m_pHost->GetBlocks();
return true;
}
bool WinMM::Start()
{
// Prepare driver thread for activity
m_bDriverLoopActive = true;
// and get it going!
m_thDriverLoop = std::thread(&WinMM::DriverLoop, this);
return true;
}
void WinMM::Stop()
{
// Signal the driver loop to exit
m_bDriverLoopActive = false;
// Wait for driver thread to exit gracefully
if (m_thDriverLoop.joinable())
m_thDriverLoop.join();
}
void WinMM::Close()
{
waveOutClose(m_hwDevice);
}
// Static Callback wrapper - specific instance is specified
void CALLBACK WinMM::waveOutProc(HWAVEOUT hWaveOut, UINT uMsg, DWORD_PTR dwInstance, DWORD dwParam1, DWORD dwParam2)
{
// All sorts of messages may be pinged here, but we're only interested
// in audio block is finished...
if (uMsg != WOM_DONE) return;
// ...which has happened so allow driver object to free resource
WinMM* driver = (WinMM*)dwInstance;
driver->FreeAudioBlock();
}
void WinMM::FreeAudioBlock()
{
// Audio subsystem is done with the block it was using, thus
// making it available again
m_nBlockFree++;
// Signal to driver loop that a block is now available. It
// could have been suspended waiting for one
std::unique_lock<std::mutex> lm(m_muxBlockNotZero);
m_cvBlockNotZero.notify_one();
}
void WinMM::DriverLoop()
{
// We will be using this vector to transfer to the host for filling, with
// user sound data (float32, -1.0 --> +1.0)
std::vector<float> vFloatBuffer(m_pHost->GetBlockSampleCount() * m_pHost->GetChannels(), 0.0f);
// While the system is active, start requesting audio data
while (m_bDriverLoopActive)
{
// Are there any blocks available to fill? ...
if (m_nBlockFree == 0)
{
// ...no, So wait until one is available
std::unique_lock<std::mutex> lm(m_muxBlockNotZero);
while (m_nBlockFree == 0) // sometimes, Windows signals incorrectly
{
// This thread will suspend until this CV is signalled
// from FreeAudioBlock.
m_cvBlockNotZero.wait(lm);
}
}
// ...yes, so use next one, by indicating one fewer
// block is available
m_nBlockFree--;
// Prepare block for processing, by oddly, marking it as unprepared :P
if (m_pWaveHeaders[m_nBlockCurrent].dwFlags & WHDR_PREPARED)
{
waveOutUnprepareHeader(m_hwDevice, &m_pWaveHeaders[m_nBlockCurrent], sizeof(WAVEHDR));
}
// Give the userland the opportunity to fill the buffer. Note that the driver
// doesnt give a hoot about timing. Thats up to the SoundWave interface to
// maintain
// Userland will populate a float buffer, that gets cleanly converted to
// a buffer of shorts for DAC
ProcessOutputBlock(vFloatBuffer, m_pvBlockMemory[m_nBlockCurrent]);
// Send block to sound device
waveOutPrepareHeader(m_hwDevice, &m_pWaveHeaders[m_nBlockCurrent], sizeof(WAVEHDR));
waveOutWrite(m_hwDevice, &m_pWaveHeaders[m_nBlockCurrent], sizeof(WAVEHDR));
m_nBlockCurrent++;
m_nBlockCurrent %= m_pHost->GetBlocks();
}
}
} // WinMM Driver Implementation
#endif
#if defined(SOUNDWAVE_USING_SDLMIXER)
namespace olc::sound::driver
{
SDLMixer* SDLMixer::instance = nullptr;
SDLMixer::SDLMixer(olc::sound::WaveEngine* pHost)
: Base(pHost)
{
instance = this;
}
SDLMixer::~SDLMixer()
{
Stop();
Close();
}
bool SDLMixer::Open(const std::string& sOutputDevice, const std::string& sInputDevice)
{
auto errc = Mix_OpenAudioDevice(static_cast<int>(m_pHost->GetSampleRate()),
AUDIO_F32,
static_cast<int>(m_pHost->GetChannels()),
static_cast<int>(m_pHost->GetBlockSampleCount()),
sOutputDevice == "DEFAULT" ? nullptr : sOutputDevice.c_str(),
SDL_AUDIO_ALLOW_FORMAT_CHANGE);
// Query the actual format of the audio device, as we have allowed it to be changed.
if (errc || !Mix_QuerySpec(nullptr, &m_haveFormat, nullptr))
{
std::cerr << "Failed to open audio device '" << sOutputDevice << "'" << std::endl;
return false;
}
// Compute the Mix_Chunk buffer's size according to the format of the audio device
Uint32 bufferSize = 0;
switch (m_haveFormat)
{
case AUDIO_F32:
case AUDIO_S32:
bufferSize = m_pHost->GetBlockSampleCount() * m_pHost->GetChannels() * 4;
break;
case AUDIO_S16:
case AUDIO_U16:
bufferSize = m_pHost->GetBlockSampleCount() * m_pHost->GetChannels() * 2;
break;
case AUDIO_S8:
case AUDIO_U8:
bufferSize = m_pHost->GetBlockSampleCount() * m_pHost->GetChannels() * 1;
break;
default:
std::cerr << "Audio format of device '" << sOutputDevice << "' is not supported" << std::endl;
return false;
}
// Allocate the buffer once. The size will never change after this
audioBuffer.resize(bufferSize);
audioChunk = {
0, // 0, as the chunk does not own the array
audioBuffer.data(), // Pointer to data array
bufferSize, // Size in bytes
128 // Volume; max by default as it's not controlled by the driver.
};
return true;
}
template<typename Int>
void ConvertFloatTo(const std::vector<float>& fromArr, Int* toArr)
{
static auto minVal = static_cast<float>(std::numeric_limits<Int>::min());
static auto maxVal = static_cast<float>(std::numeric_limits<Int>::max());
for (size_t i = 0; i != fromArr.size(); ++i)
{
toArr[i] = static_cast<Int>(std::clamp(fromArr[i] * maxVal, minVal, maxVal));
}
}
void SDLMixer::FillChunkBuffer(const std::vector<float>& userData) const
{
// Since the audio device might have changed the format we need to provide,
// we convert the wave data from the user to that format.
switch (m_haveFormat)
{
case AUDIO_F32:
memcpy(audioChunk.abuf, userData.data(), audioChunk.alen);
break;
case AUDIO_S32:
ConvertFloatTo<Sint32>(userData, reinterpret_cast<Sint32*>(audioChunk.abuf));
break;
case AUDIO_S16:
ConvertFloatTo<Sint16>(userData, reinterpret_cast<Sint16*>(audioChunk.abuf));
break;
case AUDIO_U16:
ConvertFloatTo<Uint16>(userData, reinterpret_cast<Uint16*>(audioChunk.abuf));
break;
case AUDIO_S8:
ConvertFloatTo<Sint8>(userData, reinterpret_cast<Sint8*>(audioChunk.abuf));
break;
case AUDIO_U8:
ConvertFloatTo<Uint8>(userData, audioChunk.abuf);
break;
}
}
void SDLMixer::SDLMixerCallback(int channel)
{
static std::vector<float> userData(instance->m_pHost->GetBlockSampleCount() * instance->m_pHost->GetChannels());
if (channel != 0)
{
std::cerr << "Unexpected channel number" << std::endl;
}
// Don't add another chunk if we should not keep running
if (!instance->m_keepRunning)
return;
instance->GetFullOutputBlock(userData);
instance->FillChunkBuffer(userData);
if (Mix_PlayChannel(0, &instance->audioChunk, 0) == -1)
{
std::cerr << "Error while playing Chunk" << std::endl;
}
}
bool SDLMixer::Start()
{
m_keepRunning = true;
// Kickoff the audio driver
SDLMixerCallback(0);
// SDLMixer handles all other calls to reinsert user data
Mix_ChannelFinished(SDLMixerCallback);
return true;
}
void SDLMixer::Stop()
{
m_keepRunning = false;
// Stop might be called multiple times, so we check whether the device is already closed
if (Mix_QuerySpec(nullptr, nullptr, nullptr))
{
for (int i = 0; i != m_pHost->GetChannels(); ++i)
{
if (Mix_Playing(i))
Mix_HaltChannel(i);
}
}
}
void SDLMixer::Close()
{
Mix_CloseAudio();
}
}
#endif // SOUNDWAVE_USING_SDLMIXER
#if defined(SOUNDWAVE_USING_ALSA)
// ALSA Driver Implementation
namespace olc::sound::driver
{
ALSA::ALSA(WaveEngine* pHost) : Base(pHost)
{ }
ALSA::~ALSA()
{
Stop();
Close();
}
bool ALSA::Open(const std::string& sOutputDevice, const std::string& sInputDevice)
{
// Open PCM stream
int rc = snd_pcm_open(&m_pPCM, "default", SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
// Clear global cache.
// This won't affect users who don't want to create multiple instances of this driver,
// but it will prevent valgrind from whining about "possibly lost" memory.
// If the user's ALSA setup uses a PulseAudio plugin, then valgrind will still compain
// about some "still reachable" data used by that plugin. TODO?
snd_config_update_free_global();
if (rc < 0)
return false;
// Prepare the parameter structure and set default parameters
snd_pcm_hw_params_t *params;
snd_pcm_hw_params_alloca(&params);
snd_pcm_hw_params_any(m_pPCM, params);
// Set other parameters
snd_pcm_hw_params_set_access(m_pPCM, params, SND_PCM_ACCESS_RW_INTERLEAVED);
snd_pcm_hw_params_set_format(m_pPCM, params, SND_PCM_FORMAT_FLOAT);
snd_pcm_hw_params_set_rate(m_pPCM, params, m_pHost->GetSampleRate(), 0);
snd_pcm_hw_params_set_channels(m_pPCM, params, m_pHost->GetChannels());
snd_pcm_hw_params_set_period_size(m_pPCM, params, m_pHost->GetBlockSampleCount(), 0);
snd_pcm_hw_params_set_periods(m_pPCM, params, m_pHost->GetBlocks(), 0);
// Save these parameters
rc = snd_pcm_hw_params(m_pPCM, params);
if (rc < 0)
return false;
return true;
}
bool ALSA::Start()
{
// Unsure if really needed, helped prevent underrun on my setup
std::vector<float> vSilence(m_pHost->GetBlockSampleCount() * m_pHost->GetChannels(), 0.0f);
snd_pcm_start(m_pPCM);
for (unsigned int i = 0; i < m_pHost->GetBlocks(); i++)
snd_pcm_writei(m_pPCM, vSilence.data(), m_pHost->GetBlockSampleCount());
m_rBuffers.Resize(m_pHost->GetBlocks(), m_pHost->GetBlockSampleCount() * m_pHost->GetChannels());
snd_pcm_start(m_pPCM);
m_bDriverLoopActive = true;
m_thDriverLoop = std::thread(&ALSA::DriverLoop, this);
return true;
}
void ALSA::Stop()
{
// Signal the driver loop to exit
m_bDriverLoopActive = false;
// Wait for driver thread to exit gracefully
if (m_thDriverLoop.joinable())
m_thDriverLoop.join();
if (m_pPCM != nullptr)
snd_pcm_drop(m_pPCM);
}
void ALSA::Close()
{
if (m_pPCM != nullptr)
{
snd_pcm_close(m_pPCM);
m_pPCM = nullptr;
}
// Clear the global cache again for good measure
snd_config_update_free_global();
}
void ALSA::DriverLoop()
{
const uint32_t nFrames = m_pHost->GetBlockSampleCount();
int err;
std::vector<pollfd> vFDs;
int nFDs = snd_pcm_poll_descriptors_count(m_pPCM);
if (nFDs < 0)
{
std::cerr << "snd_pcm_poll_descriptors_count returned " << nFDs << "\n";
std::cerr << "disabling polling\n";
nFDs = 0;
}
else
{
vFDs.resize(nFDs);
err = snd_pcm_poll_descriptors(m_pPCM, vFDs.data(), vFDs.size());
if (err < 0)
{
std::cerr << "snd_pcm_poll_descriptors returned " << err << "\n";
std::cerr << "disabling polling\n";
vFDs = {};
}
}
// While the system is active, start requesting audio data
while (m_bDriverLoopActive)
{
if (!m_rBuffers.IsFull())
{
// Grab some audio data
auto& vFreeBuffer = m_rBuffers.GetFreeBuffer();
GetFullOutputBlock(vFreeBuffer);
}
// Wait a bit if our buffer is full
auto avail = snd_pcm_avail_update(m_pPCM);
while (m_rBuffers.IsFull() && avail < nFrames)
{
if (vFDs.size() == 0) break;
err = poll(vFDs.data(), vFDs.size(), -1);
if (err < 0)
std::cerr << "poll returned " << err << "\n";
unsigned short revents;
err = snd_pcm_poll_descriptors_revents(m_pPCM, vFDs.data(), vFDs.size(), &revents);
if (err < 0)
std::cerr << "snd_pcm_poll_descriptors_revents returned " << err << "\n";
if (revents & POLLERR)
std::cerr << "POLLERR\n";
avail = snd_pcm_avail_update(m_pPCM);
}
// Write whatever we can
while (!m_rBuffers.IsEmpty() && avail >= nFrames)
{
auto vFullBuffer = m_rBuffers.GetFullBuffer();
uint32_t nWritten = 0;
while (nWritten < nFrames)
{
auto err = snd_pcm_writei(m_pPCM, vFullBuffer.data() + nWritten, nFrames - nWritten);
if (err > 0)
nWritten += err;
else
{
std::cerr << "snd_pcm_writei returned " << err << "\n";
break;
}
}
avail = snd_pcm_avail_update(m_pPCM);
}
}
}
} // ALSA Driver Implementation
#endif
#if defined(SOUNDWAVE_USING_PULSE)
// PULSE Driver Implementation
#include <pulse/error.h>
#include <iostream>
namespace olc::sound::driver
{
PulseAudio::PulseAudio(WaveEngine* pHost) : Base(pHost)
{ }
PulseAudio::~PulseAudio()
{
Stop();
Close();
}
bool PulseAudio::Open(const std::string& sOutputDevice, const std::string& sInputDevice)
{
pa_sample_spec ss {
PA_SAMPLE_FLOAT32, m_pHost->GetSampleRate(), (uint8_t)m_pHost->GetChannels()
};
m_pPA = pa_simple_new(NULL, "olcSoundWaveEngine", PA_STREAM_PLAYBACK, NULL,
"Output Stream", &ss, NULL, NULL, NULL);
if (m_pPA == NULL)
return false;
return true;
}
bool PulseAudio::Start()
{
m_bDriverLoopActive = true;
m_thDriverLoop = std::thread(&PulseAudio::DriverLoop, this);
return true;
}
void PulseAudio::Stop()
{
// Signal the driver loop to exit
m_bDriverLoopActive = false;
// Wait for driver thread to exit gracefully
if (m_thDriverLoop.joinable())
m_thDriverLoop.join();
}
void PulseAudio::Close()
{
if (m_pPA != nullptr)
{
pa_simple_free(m_pPA);
m_pPA = nullptr;
}
}
void PulseAudio::DriverLoop()
{
// We will be using this vector to transfer to the host for filling, with
// user sound data (float32, -1.0 --> +1.0)
std::vector<float> vFloatBuffer(m_pHost->GetBlockSampleCount() * m_pHost->GetChannels(), 0.0f);
// While the system is active, start requesting audio data
while (m_bDriverLoopActive)
{
// Grab audio data from user
GetFullOutputBlock(vFloatBuffer);
// Fill PulseAudio data buffer
int error;
if (pa_simple_write(m_pPA, vFloatBuffer.data(),
vFloatBuffer.size() * sizeof(float), &error) < 0)
{
std::cerr << "Failed to feed data to PulseAudio: " << pa_strerror(error) << "\n";
}
}
}
} // PulseAudio Driver Implementation
#endif
#endif // OLC_SOUNDWAVE IMPLEMENTATION
#endif // OLC_SOUNDWAVE_H