From 0d29d0667774cb6a9863d320c7f665dd99296609 Mon Sep 17 00:00:00 2001 From: Barracuda72 Date: Sun, 7 Jun 2020 14:50:02 +0500 Subject: [PATCH] Proper Linux support --- LinuxSoundSynthesizer/.gitignore | 7 + LinuxSoundSynthesizer/LinuxInput.h | 57 ++ LinuxSoundSynthesizer/NoiseMaker.h | 464 ++++++++++++++ LinuxSoundSynthesizer/SoundSynthesizer.cbp | 49 ++ LinuxSoundSynthesizer/main4.cpp | 672 +++++++++++++++++++++ 5 files changed, 1249 insertions(+) create mode 100644 LinuxSoundSynthesizer/.gitignore create mode 100644 LinuxSoundSynthesizer/LinuxInput.h create mode 100644 LinuxSoundSynthesizer/NoiseMaker.h create mode 100644 LinuxSoundSynthesizer/SoundSynthesizer.cbp create mode 100644 LinuxSoundSynthesizer/main4.cpp diff --git a/LinuxSoundSynthesizer/.gitignore b/LinuxSoundSynthesizer/.gitignore new file mode 100644 index 0000000..3c4f2f2 --- /dev/null +++ b/LinuxSoundSynthesizer/.gitignore @@ -0,0 +1,7 @@ +# Ignore binary and object files +bin/ +obj/ + +# Ignore internal IDE stuff +*.layout +*.depend diff --git a/LinuxSoundSynthesizer/LinuxInput.h b/LinuxSoundSynthesizer/LinuxInput.h new file mode 100644 index 0000000..5dd0b6f --- /dev/null +++ b/LinuxSoundSynthesizer/LinuxInput.h @@ -0,0 +1,57 @@ +#ifndef LINUXINPUT_H_INCLUDED +#define LINUXINPUT_H_INCLUDED + +// Linux async input routines +// To use this, your user must be in "input" group: +// sudo usermod -aG input + +#include +#include +#include + +// Start key input poll +FILE* start_input() +{ + return fopen("/dev/input/by-path/platform-i8042-serio-0-event-kbd", "rb"); +} + +// Retrieve all keys +void read_keys(FILE* input, char* key_map) +{ + memset(key_map, 0, KEY_MAX/8 + 1); // Initate the array to zero's + ioctl(fileno(input), EVIOCGKEY(KEY_MAX/8 + 1), key_map); +} + +char* get_all_keys(FILE* input) +{ + char* key_map = (char*)malloc(KEY_MAX/8 + 1); // Create a byte array the size of the number of keys + + read_keys(input, key_map); + + return key_map; +} + +// Retrieve specific key +int check_key_state(char* key_map, int key) +{ + int keyb = key_map[key/8]; // The key we want (and the seven others arround it) + int mask = 1 << (key % 8); // Put a one in the same column as out key state will be in; + + return (keyb & mask) != 0; // Returns true if pressed otherwise false +} + +int get_key_state(FILE* input, int key) +{ + char key_map[KEY_MAX/8 + 1]; + + read_keys(input, key_map); + + return check_key_state(key_map, key); +} + +void stop_input(FILE* input) +{ + fclose(input); +} + +#endif // LINUXINPUT_H_INCLUDED diff --git a/LinuxSoundSynthesizer/NoiseMaker.h b/LinuxSoundSynthesizer/NoiseMaker.h new file mode 100644 index 0000000..404d46d --- /dev/null +++ b/LinuxSoundSynthesizer/NoiseMaker.h @@ -0,0 +1,464 @@ +#ifndef NOISEMAKER_H_INCLUDED +#define NOISEMAKER_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +// Linux-specific stuff begins +#include + +using namespace std; + +template +class NoiseMaker +{ +public: + NoiseMaker(wstring sOutputDevice, unsigned int nSampleRate = 44100, unsigned int nChannels = 1, unsigned int nBlocks = 32, unsigned int nSamples = 2048) + { + Create(sOutputDevice, nSampleRate, nChannels, nBlocks, nSamples); + } + + ~NoiseMaker() + { + Destroy(); + } + + bool Create(const wstring sOutputDevice, unsigned int nSampleRate = 44100, unsigned int nChannels = 1, unsigned int nBlocks = 32, unsigned int nSamples = 2048) + { + m_bReady = false; + m_nSampleRate = nSampleRate; + m_nChannels = nChannels; + m_nBlockCount = nBlocks; + m_nBlockSamples = nSamples; + m_nBlockFree = m_nBlockCount; + m_nBlockCurrent = 0; + m_pBlockMemory = nullptr; + + m_userFunction = nullptr; + + // Validate device + vector devices = Enumerate(); + auto d = std::find(devices.begin(), devices.end(), sOutputDevice); + + if (d != devices.end()) + { + std::wstring_convert> converter; + + // Check and init device + snd_pcm_hw_params_t *hw_params; + + // Open ALSA device by it's name in playback mode + snd_pcm_open(&m_hwDevice, converter.to_bytes(sOutputDevice).c_str(), SND_PCM_STREAM_PLAYBACK, 0); + // Allocate memory for the parameters and initialize it with default values + snd_pcm_hw_params_malloc(&hw_params); + snd_pcm_hw_params_any(m_hwDevice, hw_params); + + // Tell the ALSA we are going to use it with indirect buffered writes (calls to snd_pcm_writei) + // in interleaved mode (samples for different channels are placed in the buffer next to each other). + // The other options include indirect non-interleaved mode (samples for different channels placed + // in differrent halves/quarters/etc of the buffer) and direct writes to memory shared with sound + // hardware (also in interleaved / non-interleaved manner). + // (Not that all that matters for us with our mono output) + if (snd_pcm_hw_params_set_access(m_hwDevice, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED) < 0) { + cout << "Cannot set access type" << endl; + return false; + } + + // Specify asynchronous handler for audio events + if (snd_async_add_pcm_handler(&m_aHandler, m_hwDevice, alsaProcWrap, this) < 0) + { + cout << "Unable to register async handler" << endl; + return false; + } + + // Determine and set samples format (16-bit shorts, 32-bit floats, etc) + snd_pcm_format_t nFormat = GetAlsaPcmFormat(); + snd_pcm_hw_params_set_format(m_hwDevice, hw_params, nFormat); + + // Set the sample rate. Beware that ALSA might choose sample rate + // slightly different (but close) to that we supplied (whence the "rate_near"), + // so in real world it's good to check nSampleRate after function call + snd_pcm_hw_params_set_rate_near(m_hwDevice, hw_params, &nSampleRate, 0); + // Set the number of channels + snd_pcm_hw_params_set_channels(m_hwDevice, hw_params, nChannels); + // Pass the parameters to ALSA + snd_pcm_hw_params(m_hwDevice, hw_params); + // Dispose memory occupied by the parameters structure + snd_pcm_hw_params_free(hw_params); + // Prepare sound card for interaction + if (snd_pcm_prepare(m_hwDevice) < 0) + { + cout << "Failed to prepare sound interface" << endl; + return false; + } + } + + // Allocate block memory + m_pBlockMemory = new T[m_nBlockCount * m_nBlockSamples]; + if (m_pBlockMemory == nullptr) + return Destroy(); + bzero(m_pBlockMemory, m_nBlockCount * m_nBlockSamples * sizeof(T)); + + // ALSA don't need Wave headers, so that's it + + m_bReady = true; + + m_thread = thread(&NoiseMaker::MainThread, this); + + unique_lock lm(m_muxBlockNotZero); + m_cvBlockNotZero.notify_one(); + + return true; + } + + bool Destroy() + { + return false; + } + + bool IsOK() + { + return true; + } + + void Start() + { + } + + void Stop() + { + m_bReady = false; + m_thread.join(); + } + + double GetTime() + { + return m_dTime; + } + + double mix(double dSample1, double dAmp1, double dSample2, double dAmp2) + { + return (dSample1*dAmp1) + (dSample2*dAmp2); + } + + double clip(double dSample, double dMax) + { + if (dSample >= 0.0) + return fmin(dSample, dMax); + else + return fmax(dSample, -dMax); + } + + double amplify(double dSample, double dGain) + { + return dSample * dGain; + } + + double sgn(double d) + { + return 2.0 * (0.0 < d) - 1.0; + } + + double osc(double dTime, double dAmp, double dFreq, double dPhase = 0.0) + { + return dAmp * sin(dTime * 2.0 * M_PI * dFreq + dPhase); + } + + double tri(double dTime, double dAmp, double dFreq, double dPhase = 0.0) + { + return ((dAmp * 2.0) / M_PI) * asin(osc(dTime, dAmp, dFreq, dPhase)); + } + + double sqr(double dTime, double dAmp, double dFreq, double dPhase = 0.0) + { + double d = osc(dTime, dAmp, dFreq, dPhase); + return d * sgn(d); + } + + double noise(double dTime = 0.0, double dAmp = 0.0, double dFreq = 0.0, double dPhase = 0.0) + { + return dAmp * ((double)rand() / (double)RAND_MAX); + } + + // Override to process current sample + virtual double UserProcess(int nChannel, double dTime) + { + double dWeird = sqr(dTime, 1.0, 0.3) * ((tri(dTime, 1.0, sqr(dTime, 4.0, 5.0) + 440.0, osc(dTime, 0 * M_PI, 0.5)) + osc(dTime, 1.0, 440.0))); + WriteBuffer(0, dWeird); + double dDelay = ReadBuffer(0); + return mix(dWeird, 0.3, dDelay, 0.3); + } + + // Override to create buffers, tools, other long-term fixtures + virtual bool UserConstructBuffer() + { + ConstructBuffer(0, 0.05); + return true; + } + + struct NoiseBuffer + { + int id; + double dLength; + double dAttenuation; + unsigned int nSamples; + unsigned int nFront; + unsigned int nBack; + double* buffer; + + double read() + { + double d = buffer[nBack]; + nBack++; + nBack %= nSamples; + return d; + } + + void write(double d) + { + buffer[nFront] = d; + nFront++; + nFront %= nSamples; + } + }; + + void WriteBuffer(int id, double d) + { + m_buffers[id].write(d); + } + + double ReadBuffer(int id) + { + return m_buffers[id].read(); + } + + bool ConstructBuffer(int id, double dLength, double dAttenuation = 1.0) + { + NoiseBuffer b; + b.id = id; + b.dLength = dLength; + b.dAttenuation = dAttenuation; + b.nSamples = (unsigned int)((double)m_nSampleRate * dLength); + b.nFront = 0; + b.nBack = 1; + b.buffer = new double[b.nSamples]; + + m_buffers.push_back(b); + + return true; + } + + static vector Enumerate() + { + vector sDevices; + + std::wstring_convert> converter; // string <-> wide string + + // Linux-specific ALSA stuff + + // Device "default" is always there + sDevices.push_back(L"default"); + + char **hints; + + // Get list of all devices that is capable of playback + int err = snd_device_name_hint(-1, "pcm", (void***)&hints); + if (err == 0) + { + char** n = hints; + while (*n != NULL) + { + // Get device name + char *name = snd_device_name_get_hint(*n, "NAME"); + + if (name != NULL) + { + if (strncmp("null", name, 4) != 0) + sDevices.push_back(converter.from_bytes(name)); + free(name); + } + n++; + } + snd_device_name_free_hint((void**)hints); + } + + return sDevices; + } + + void SetUserFunction(double (*func)(int, double)) + { + m_userFunction = func; + } + +private: + double (*m_userFunction)(int, double); + + unsigned int m_nSampleRate; + unsigned int m_nChannels; + unsigned int m_nBlockCount; + unsigned int m_nBlockSamples; + unsigned int m_nBlockCurrent; + + vector m_buffers; + + T* m_pBlockMemory; + // waveHeaders + snd_pcm_t* m_hwDevice; + snd_async_handler_t* m_aHandler; + + thread m_thread; + atomic m_bReady; + atomic m_nBlockFree; + condition_variable m_cvBlockNotZero; + mutex m_muxBlockNotZero; + + atomic m_dTime; + + // A bit of juicy function what helps to determine correct PCM format for our buffer + static snd_pcm_format_t GetAlsaPcmFormat() + { + if (std::is_signed::value) + { + if (std::is_floating_point::value) + { + switch (sizeof(T)) + { + case 4: // Float + return SND_PCM_FORMAT_FLOAT_LE; + + case 8: // Double + default: + return SND_PCM_FORMAT_FLOAT64_LE; + } + } else { + switch (sizeof(T)) + { + case 1: // Signed byte + return SND_PCM_FORMAT_S8; + + case 2: // Signed short + return SND_PCM_FORMAT_S16_LE; + + case 4: // Signed int + default: + return SND_PCM_FORMAT_S32_LE; + } + } + } else { + switch (sizeof(T)) + { + case 1: // Unsigned byte + return SND_PCM_FORMAT_U8; + + case 2: // Unsigned short + return SND_PCM_FORMAT_U16_LE; + + case 4: // Unsigned int + default: + return SND_PCM_FORMAT_U32_LE; + } + } + } + + // Hander for soundcard events + void alsaProc(/* No parameters */) + { + snd_pcm_state_t state = snd_pcm_state(m_hwDevice); + // Handle underrun + if (state == SND_PCM_STATE_XRUN) + { + int err = snd_pcm_prepare(m_hwDevice); + if (err < 0) + { + // Couldn't recovery; exiting + exit(1); + } + } + + m_nBlockFree++; + unique_lock lm(m_muxBlockNotZero); + m_cvBlockNotZero.notify_one(); + } + + // Static wrapper for sound card handler + static void alsaProcWrap(snd_async_handler_t* ahandler) + { + NoiseMaker* data = (NoiseMaker*)snd_async_handler_get_callback_private(ahandler); + data->alsaProc(); + } + + void MainThread() + { + m_dTime = 0.0; + double dTimeStep = 1.0 / (double)m_nSampleRate; + + T nMaxSample = (T)pow(2, (sizeof(T) * 8) - 1) - 1; + + double dMaxSample = (double)nMaxSample; + + UserConstructBuffer(); + + T nPreviousSample = 0; + + while (m_bReady) + { + // Wait for the block to become available + if (m_nBlockFree == 0) + { + unique_lock lm(m_muxBlockNotZero); + m_cvBlockNotZero.wait(lm); + } + + // Block is here, use it + m_nBlockFree--; + + // Process block + int nCurrentBlock = m_nBlockCurrent * m_nBlockSamples; + bool bGlitchDetected = false; + T nGlitchThreshold = 400; + T nNewSample = 0; + int nSampleCount = 0; + + for (unsigned int n = 0; n < m_nBlockSamples; n += m_nChannels) + { + // Update buffers + + for (int c = 0; c < m_nChannels; c++) + { + // User process + if (m_userFunction == nullptr) + nNewSample = (T)(clip(UserProcess(c, m_dTime), 1.0) * dMaxSample); + else + nNewSample = (T)(clip(m_userFunction(c, m_dTime), 1.0) * dMaxSample); + + m_pBlockMemory[nCurrentBlock + n + c] = nNewSample; + nPreviousSample = nNewSample; + } + m_dTime = m_dTime + dTimeStep; + } + + int err; + // Send block to sound device + // All the errors will be handled in async callback + snd_pcm_writei(m_hwDevice, &m_pBlockMemory[nCurrentBlock], m_nBlockSamples); + + snd_pcm_start(m_hwDevice); + m_nBlockCurrent++; + m_nBlockCurrent %= m_nBlockCount; + } + } +}; + +#endif // NOISEMAKER_H_INCLUDED diff --git a/LinuxSoundSynthesizer/SoundSynthesizer.cbp b/LinuxSoundSynthesizer/SoundSynthesizer.cbp new file mode 100644 index 0000000..7449833 --- /dev/null +++ b/LinuxSoundSynthesizer/SoundSynthesizer.cbp @@ -0,0 +1,49 @@ + + + + + + diff --git a/LinuxSoundSynthesizer/main4.cpp b/LinuxSoundSynthesizer/main4.cpp new file mode 100644 index 0000000..412de2f --- /dev/null +++ b/LinuxSoundSynthesizer/main4.cpp @@ -0,0 +1,672 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace std; + +#include "NoiseMaker.h" +#include "LinuxInput.h" + +#define FTYPE double + +namespace synth +{ + // Utilities + + // Forward declaration + struct instrument_base; + + // Converts frequency (hertz) to angular velocity + FTYPE w(const FTYPE dHertz) + { + return dHertz * 2.0 * M_PI; + } + + // A basic note + struct note + { + int id; // Position in scale + FTYPE on; // Time note was activated + FTYPE off; // Time note was deactivated + bool active; + instrument_base* channel; + + note() + { + id = 0; + channel = 0; + active = false; + on = 0.0; + off = 0.0; + } + }; + + // General purpose oscillator + enum OSC_TYPE + { + OSC_SINE = 0, + OSC_SQUARE, + OSC_TRIANGLE, + OSC_SAW_ANA, + OSC_SAW_DIG, + OSC_NOISE, + }; + + FTYPE osc(FTYPE dHertz, FTYPE dTime, OSC_TYPE nType = OSC_SINE, FTYPE dLFOHertz = 0.0, FTYPE dLFOAmplitude = 0.0, FTYPE dCustom = 50.0) + { + FTYPE dFreq = w(dHertz) * dTime + dLFOAmplitude * dHertz * sin(w(dLFOHertz) * dTime); + + switch(nType) + { + case OSC_SINE: // Sine wave + return sin(dFreq); + + case OSC_SQUARE: // Square wave + return sin(dFreq) > 0.0 ? 1.0 : -1.0; + + case OSC_TRIANGLE: // Triangle wave + return asin(sin(dFreq)) * 2.0 / M_PI; + + case OSC_SAW_ANA: // Saw wave (analogue) + { + FTYPE dOutput = 0.0; + for (int n = 1; n < 100; n++) + dOutput += sin(n * dFreq) / (FTYPE)n; + return dOutput * (2.0 / M_PI); + } + + case OSC_SAW_DIG: // Saw wave (digital) + return (2.0 / M_PI) * (dHertz * M_PI * fmod(dTime, 1.0 / dHertz) - (M_PI / 2.0)); + + case OSC_NOISE: // Pseudo random noise + return 2.0 * ((FTYPE)rand() / (FTYPE)RAND_MAX) - 1.0; + + default: + return 0.0; + } + } + + // Scale to frequency conversion + enum SCALE_TYPE + { + SCALE_DEFAULT = 0, + }; + + FTYPE scale(const int nNoteID, const SCALE_TYPE nScaleID = SCALE_DEFAULT) + { + return 256 * pow(1.0594630943592952645618252949463, nNoteID); + } + + // Envelope + struct envelope + { + virtual FTYPE amplitude(const FTYPE dTime, const FTYPE dTimeOn, const FTYPE dTimeOff) = 0; + }; + + struct envelope_adsr : public envelope + { + FTYPE dAttackTime; + FTYPE dDecayTime; + FTYPE dReleaseTime; + + FTYPE dSustainAmplitude; + FTYPE dStartAmplitude; + + envelope_adsr() + { + dAttackTime = 0.1; + dDecayTime = 0.1; + dStartAmplitude = 1.0; + dSustainAmplitude = 1.0; + dReleaseTime = 0.2; + } + + virtual FTYPE amplitude(const FTYPE dTime, const FTYPE dTimeOn, const FTYPE dTimeOff) + { + FTYPE dAmplitude = 0.0; + FTYPE dReleaseAmplitude = 0.0; + + if (dTimeOn > dTimeOff) // Note is on + { + // ADS + FTYPE dLifeTime = dTime - dTimeOn; + + // Attack phase + if (dLifeTime <= dAttackTime) + dAmplitude = (dLifeTime / dAttackTime) * dStartAmplitude; + + // Decay phase + if (dLifeTime > dAttackTime && dLifeTime <= (dAttackTime + dDecayTime)) + dAmplitude = ((dLifeTime - dAttackTime) / dDecayTime) * (dSustainAmplitude - dStartAmplitude) + dStartAmplitude; + + // Sustain phase + if (dLifeTime > (dAttackTime + dDecayTime)) + dAmplitude = dSustainAmplitude; + } else { + // R + FTYPE dLifeTime = dTimeOff - dTimeOn; + + // Release + if (dLifeTime <= dAttackTime) + dReleaseAmplitude = (dLifeTime / dAttackTime) * dStartAmplitude; + + if (dLifeTime > dAttackTime && dLifeTime <= (dAttackTime + dDecayTime)) + dReleaseAmplitude = ((dLifeTime - dAttackTime) / dDecayTime) * (dSustainAmplitude - dStartAmplitude) + dStartAmplitude; + + // Sustain phase + if (dLifeTime > (dAttackTime + dDecayTime)) + dReleaseAmplitude = dSustainAmplitude; + + dAmplitude = ((dTime - dTimeOff) / dReleaseTime) * (0.0 - dReleaseAmplitude) + dReleaseAmplitude; + } + + if (dAmplitude <= 0.0001) + dAmplitude = 0.0; + + return dAmplitude; + } + }; + + FTYPE env(const FTYPE dTime, envelope& envlpe, const FTYPE dTimeOn, const FTYPE dTimeOff) + { + return envlpe.amplitude(dTime, dTimeOn, dTimeOff); + } + + struct instrument_base + { + FTYPE dVolume; + synth::envelope_adsr env; + FTYPE fMaxLifeTime; + wstring name; + + virtual FTYPE sound(const FTYPE dTime, synth::note n, bool& bNoteFinished) = 0; + }; + + struct instrument_bell : public instrument_base + { + instrument_bell() + { + env.dAttackTime = 0.01; + env.dDecayTime = 1.0; + env.dSustainAmplitude = 0.0; + env.dReleaseTime = 1.0; + fMaxLifeTime = 3.0; + name = L"Bell"; + + dVolume = 1.0; + } + + virtual FTYPE sound(const FTYPE dTime, synth::note n, bool& bNoteFinished) + { + FTYPE dAmplitude = synth::env(dTime, env, n.on, n.off); + if (dAmplitude <= 0.0) + bNoteFinished = true; + + FTYPE dSound = + + 1.00 * synth::osc(n.on - dTime, synth::scale(n.id + 12), synth::OSC_SINE, 5.0, 0.001) + + 0.50 * synth::osc(n.on - dTime, synth::scale(n.id + 24)) + + 0.25 * synth::osc(n.on - dTime, synth::scale(n.id + 36)); + + return dAmplitude * dSound * dVolume; + } + }; + + struct instrument_bell8 : public instrument_base + { + instrument_bell8() + { + env.dAttackTime = 0.01; + env.dDecayTime = 0.5; + env.dSustainAmplitude = 0.8; + env.dReleaseTime = 1.0; + fMaxLifeTime = 3.0; + name = L"8-bit Bell"; + + dVolume = 1.0; + } + + virtual FTYPE sound(const FTYPE dTime, synth::note n, bool& bNoteFinished) + { + FTYPE dAmplitude = synth::env(dTime, env, n.on, n.off); + if (dAmplitude <= 0.0) + bNoteFinished = true; + + FTYPE dSound = + + 1.00 * synth::osc(n.on - dTime, synth::scale(n.id + 0 ), synth::OSC_SQUARE, 5.0, 0.001) + + 0.50 * synth::osc(n.on - dTime, synth::scale(n.id + 12)) + + 0.25 * synth::osc(n.on - dTime, synth::scale(n.id + 24)); + + return dAmplitude * dSound * dVolume; + } + }; + + struct instrument_harmonica : public instrument_base + { + instrument_harmonica() + { + env.dAttackTime = 0.05; + env.dDecayTime = 1.0; + env.dSustainAmplitude = 0.95; + env.dReleaseTime = 0.1; + fMaxLifeTime = -1.0; + name = L"Harmonica"; + + dVolume = 1.0; + } + + virtual FTYPE sound(const FTYPE dTime, synth::note n, bool& bNoteFinished) + { + FTYPE dAmplitude = synth::env(dTime, env, n.on, n.off); + if (dAmplitude <= 0.0) + bNoteFinished = true; + + FTYPE dSound = + + 1.00 * synth::osc(n.on - dTime, synth::scale(n.id + 0 ), synth::OSC_SQUARE, 5.0, 0.001) + + 0.50 * synth::osc(n.on - dTime, synth::scale(n.id + 12), synth::OSC_SQUARE) + + 0.25 * synth::osc(n.on - dTime, synth::scale(n.id + 24), synth::OSC_SQUARE); + + return dAmplitude * dSound * dVolume; + } + }; + + struct instrument_drumkick : public instrument_base + { + instrument_drumkick() + { + env.dAttackTime = 0.01; + env.dDecayTime = 0.15; + env.dSustainAmplitude = 0.0; + env.dReleaseTime = 0.0; + fMaxLifeTime = 1.5; + name = L"Kick Drum"; + + dVolume = 1.0; + } + + virtual FTYPE sound(const FTYPE dTime, synth::note n, bool& bNoteFinished) + { + FTYPE dAmplitude = synth::env(dTime, env, n.on, n.off); + if (fMaxLifeTime > 0.0 && dTime - n.on >= fMaxLifeTime) + bNoteFinished = true; + + FTYPE dSound = + + 0.99 * synth::osc(dTime - n.on, synth::scale(n.id - 36), synth::OSC_SINE, 1.0, 1.0) + + 0.01 * synth::osc(0.0, 0, synth::OSC_NOISE); + + return dAmplitude * dSound * dVolume; + } + }; + + struct instrument_drumsnare : public instrument_base + { + instrument_drumsnare() + { + env.dAttackTime = 0.0; + env.dDecayTime = 0.2; + env.dSustainAmplitude = 0.0; + env.dReleaseTime = 0.0; + fMaxLifeTime = 1.0; + name = L"Snare Drum"; + + dVolume = 1.0; + } + + virtual FTYPE sound(const FTYPE dTime, synth::note n, bool& bNoteFinished) + { + FTYPE dAmplitude = synth::env(dTime, env, n.on, n.off); + if (fMaxLifeTime > 0.0 && dTime - n.on >= fMaxLifeTime) + bNoteFinished = true; + + FTYPE dSound = + + 0.50 * synth::osc(dTime - n.on, synth::scale(n.id - 24), synth::OSC_SINE, 0.5, 1.0) + + 0.50 * synth::osc(dTime - n.on, 0, synth::OSC_NOISE); + + return dAmplitude * dSound * dVolume; + } + }; + + struct instrument_drumhihat : public instrument_base + { + instrument_drumhihat() + { + env.dAttackTime = 0.01; + env.dDecayTime = 0.05; + env.dSustainAmplitude = 0.0; + env.dReleaseTime = 0.0; + fMaxLifeTime = 1.0; + name = L"HiHat Drum"; + + dVolume = 1.0; + } + + virtual FTYPE sound(const FTYPE dTime, synth::note n, bool& bNoteFinished) + { + FTYPE dAmplitude = synth::env(dTime, env, n.on, n.off); + if (fMaxLifeTime > 0.0 && dTime - n.on >= fMaxLifeTime) + bNoteFinished = true; + + FTYPE dSound = + + 0.1 * synth::osc(dTime - n.on, synth::scale(n.id - 12), synth::OSC_SQUARE, 1.5, 1.0) + + 0.9 * synth::osc(0.0, 0, synth::OSC_NOISE); + + return dAmplitude * dSound * dVolume; + } + }; + + struct sequencer + { + public: + struct channel + { + instrument_base* instrument; + wstring sBeat; + }; + + sequencer(float tempo = 120.0f, int beats = 4, int subbeats = 4) + { + nBeats = beats; + nSubBeats = subbeats; + fTempo = tempo; + nCurrentBeat = 0; + fBeatTime = (60.0f / fTempo) / (float)nSubBeats; + nTotalBeats = nBeats*nSubBeats; + fAccumulate = 0.0f; + } + + int Update(FTYPE fElapsedTime) + { + vecNotes.clear(); + + fAccumulate += fElapsedTime; + + while (fAccumulate >= fBeatTime) + { + fAccumulate -= fBeatTime; + nCurrentBeat++; + + if (nCurrentBeat >= nTotalBeats) + nCurrentBeat = 0; + + int c = 0; + + for (auto& v : vecChannel) + { + if (v.sBeat[nCurrentBeat] == L'X') + { + note n; + + n.channel = vecChannel[c].instrument; + n.active = true; + n.id = 64; + + vecNotes.push_back(n); + } + c++; + } + } + + return vecNotes.size(); + } + + void AddInstrument(instrument_base* inst) + { + channel c; + c.instrument = inst; + vecChannel.push_back(c); + } + + public: + int nBeats; + int nSubBeats; + float fTempo; + float fBeatTime; + int nCurrentBeat; + int nTotalBeats; + float fAccumulate; + vector vecNotes; + vector vecChannel; + }; +} + +// Global synthesizer variables +synth::instrument_base* voice = nullptr; +mutex muxNotes; +vector vecNotes; + +synth::instrument_bell instBell; +synth::instrument_harmonica instHarm; +synth::instrument_drumkick instKick; +synth::instrument_drumsnare instSnare; +synth::instrument_drumhihat instHiHat; + +typedef bool(*lambda)(synth::note const& item); +template +void safe_remove(T& v, lambda f) +{ + auto n = v.begin(); + + while (n != v.end()) + { + if (!f(*n)) + n = v.erase(n); + else + ++n; + } +} + +FTYPE MakeNoise(int nChannel, FTYPE dTime) +{ + unique_lock lm(muxNotes); + FTYPE dMixedOutput = 0.0; + + for (auto& n : vecNotes) + { + bool bNoteFinished = false; + FTYPE dSound = 0.0; + + if (n.channel != nullptr) + dSound = n.channel->sound(dTime, n, bNoteFinished); + + dMixedOutput += dSound; + + if (bNoteFinished) + n.active = false; + } + + safe_remove>(vecNotes, [](synth::note const& item){return item.active;}); + + return dMixedOutput * 0.1; +} + +int main() +{ + WINDOW *w = initscr(); + cbreak(); + nodelay(w, TRUE); + noecho(); + + vector devices = NoiseMaker::Enumerate(); + + for (auto& d : devices) + printw("Found Output Device: %ls\n", d.c_str()); + wrefresh(w); + + NoiseMaker sound(devices[0], 44100, 1, 8, 1024); + + voice = new synth::instrument_harmonica();//new bell(); + + sound.SetUserFunction(MakeNoise); + + auto draw = [&w](int x, int y, wstring ws) + { + using convert_type = std::codecvt_utf8; + std::wstring_convert converter; + + std::string s = converter.to_bytes( ws ); + mvwaddstr(w, y, x, s.c_str()); + }; + + int keys[16] = { + KEY_Z, + KEY_S, + KEY_X, + KEY_C, + KEY_F, + KEY_V, + KEY_G, + KEY_B, + KEY_N, + KEY_J, + KEY_M, + KEY_K, + KEY_COMMA, + KEY_L, + KEY_DOT, + KEY_SLASH, + }; + + FILE* input = start_input(); + + char* all_keys = get_all_keys(input); + + auto clock_old_time = chrono::high_resolution_clock::now(); + auto clock_real_time = chrono::high_resolution_clock::now(); + double dElapsedTime = 0.0; + double dWallTime = 0.0; + + // Establish sequencer + synth::sequencer seq(90.0); + + seq.AddInstrument(&instKick); + seq.AddInstrument(&instSnare); + seq.AddInstrument(&instHiHat); + + seq.vecChannel.at(0).sBeat = L"X...X...X..X.X.."; + seq.vecChannel.at(1).sBeat = L"..X...X...X...X."; + seq.vecChannel.at(2).sBeat = L"X.X.X.X.X.X.X.XX"; + + while (1) + { + // Update timings + clock_real_time = chrono::high_resolution_clock::now(); + auto time_last_loop = clock_real_time - clock_old_time; + clock_old_time = clock_real_time; + dElapsedTime = chrono::duration(time_last_loop).count(); + dWallTime += dElapsedTime; + FTYPE dTimeNow = sound.GetTime(); + + // Sequencer + int newNotes = seq.Update(dElapsedTime); + muxNotes.lock(); + for (int a = 0; a < newNotes; a++) + { + seq.vecNotes[a].on = dTimeNow; + vecNotes.emplace_back(seq.vecNotes[a]); + } + muxNotes.unlock(); + + read_keys(input, all_keys); + for (int k = 0; k < 16; k++) + { + int nKeyState = check_key_state(all_keys, keys[k]); + double dTimeNow = sound.GetTime(); + muxNotes.lock(); + auto noteFound = find_if(vecNotes.begin(), vecNotes.end(), [&k](synth::note const& item){return item.id == k;}); + if (noteFound == vecNotes.end()) + { + // Note not found in vector + if (nKeyState) + { + // Key pressed so create a new note + synth::note n; + n.id = k; + n.on = dTimeNow; + //n.channel = voice; + n.channel = &instHarm; + n.active = true; + + vecNotes.emplace_back(n); + } else { + // Note not in vector, but key hasn't been pressed + // Nothing to do + } + } else { + // Note exists in the vector + if (nKeyState) + { + if (noteFound->off > noteFound->on) + { + noteFound->on = dTimeNow; + noteFound->active = true; + } + } else { + if (noteFound->off < noteFound->on) + { + noteFound->off = dTimeNow; + } + } + } + muxNotes.unlock(); + } + + // VISUAL + // Clear Background + werase(w); + + // Draw Sequencer + draw(2, 2, L"SEQUENCER:"); + for (int beats = 0; beats < seq.nBeats; beats++) + { + draw(beats*seq.nSubBeats + 20, 2, L"O"); + for (int subbeats = 1; subbeats < seq.nSubBeats; subbeats++) + draw(beats*seq.nSubBeats + subbeats + 20, 2, L"."); + } + + // Draw Sequences + int n = 0; + for (auto v : seq.vecChannel) + { + draw(2, 3 + n, v.instrument->name); + draw(20, 3 + n, v.sBeat); + n++; + } + + // Draw Beat Cursor + draw(20 + seq.nCurrentBeat, 1, L"|"); + + // Draw Keyboard + draw(2, 8, L"| | | | | | | | | | | | | | | | | "); + draw(2, 9, L"| | S | | | F | | G | | | J | | K | | L | | | "); + draw(2, 10, L"| |___| | |___| |___| | |___| |___| |___| | |__"); + draw(2, 11, L"| | | | | | | | | | |"); + draw(2, 12, L"| Z | X | C | V | B | N | M | , | . | / |"); + draw(2, 13, L"|_____|_____|_____|_____|_____|_____|_____|_____|_____|_____|"); + + // Draw Stats + wstring stats = L"Notes: " + to_wstring(vecNotes.size()) + L" Wall Time: " + to_wstring(dWallTime) + L" CPU Time: " + to_wstring(dTimeNow) + L" Latency: " + to_wstring(dWallTime - dTimeNow) ; + draw(2, 15, stats); + + wrefresh(w); + // /VISUAL + + if (get_key_state(input, KEY_Q)) + break; + } + + stop_input(input); + free(all_keys); + + sound.Stop(); + delete voice; + // Little hack: read all the characters that was produced on stdin so they won't appear in commamd prompt + while (getch() != ERR); + endwin(); + + return 0; +}