diff --git a/.github/workflows/emscripten-build.yml b/.github/workflows/emscripten-build.yml new file mode 100644 index 0000000..f31ee26 --- /dev/null +++ b/.github/workflows/emscripten-build.yml @@ -0,0 +1,43 @@ +name: Emscripten Build + +on: + push: + branches: + - develop + pull_request: + branches: + - develop + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout source code + uses: actions/checkout@v3 + with: + submodules: 'recursive' + + - name: Set up Python (required for Emscripten) + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install Emscripten SDK + run: | + git clone https://github.com/emscripten-core/emsdk.git + cd emsdk + ./emsdk install latest + ./emsdk activate latest + source ./emsdk_env.sh + shell: bash + + - name: Build with Emscripten + run: | + source emsdk/emsdk_env.sh + cd demo + make emscripten + shell: bash + + diff --git a/.github/workflows/emscripten-deploy.yml b/.github/workflows/emscripten-deploy.yml new file mode 100644 index 0000000..f293005 --- /dev/null +++ b/.github/workflows/emscripten-deploy.yml @@ -0,0 +1,70 @@ +name: Emscripten Deploy + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout source code + uses: actions/checkout@v3 + with: + submodules: 'recursive' + + - name: Set up Python (required for Emscripten) + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install Emscripten SDK + run: | + git clone https://github.com/emscripten-core/emsdk.git + cd emsdk + ./emsdk install latest + ./emsdk activate latest + source ./emsdk_env.sh + shell: bash + + - name: Build with Emscripten + run: | + source emsdk/emsdk_env.sh + cd demo + make emscripten + shell: bash + + - name: Deploy Main Demo + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.REMOTE_SERVER_ADDR }} + username: ${{ secrets.REMOTE_SERVER_USER }} + key: ${{ secrets.REMOTE_SERVER_SSH_KEY }} + port: ${{ secrets.REMOTE_SERVER_PORT }} + source: "demo/demo.html,demo/demo.js,demo/demo.wasm,demo/demo.data" + target: ${{ secrets.REMOTE_SERVER_DIRECTORY }} + strip_components: 1 + + - name: Deploy Waveform Demo + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.REMOTE_SERVER_ADDR }} + username: ${{ secrets.REMOTE_SERVER_USER }} + key: ${{ secrets.REMOTE_SERVER_SSH_KEY }} + port: ${{ secrets.REMOTE_SERVER_PORT }} + source: "demo/demo_waveform.html,demo/demo_waveform.js,demo/demo_waveform.wasm,demo/demo_waveform.data" + target: ${{ secrets.REMOTE_SERVER_DIRECTORY }} + strip_components: 1 + + - name: Deploy Synthesis Demo + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.REMOTE_SERVER_ADDR }} + username: ${{ secrets.REMOTE_SERVER_USER }} + key: ${{ secrets.REMOTE_SERVER_SSH_KEY }} + port: ${{ secrets.REMOTE_SERVER_PORT }} + source: "demo/demo_synthesis.html,demo/demo_synthesis.js,demo/demo_synthesis.wasm,demo/demo_synthesis.data" + target: ${{ secrets.REMOTE_SERVER_DIRECTORY }} + strip_components: 1 diff --git a/README.md b/README.md index 6c38aa5..ca4f793 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,10 @@ files. Because it's built on top of [miniaudio](https://miniaud.io), it requires to no additional build configurations in order to be built for cross-platform. -See the emscripten version [running on the web here](https://www.moros1138.com/demos/olcPGEX_MiniAudio/). +See the emscripten versions of the demos here: +* [Main Demo](https://www.moros1138.com/demos/olcPGEX_MiniAudio/). +* [Waveform Demo](https://www.moros1138.com/demos/olcPGEX_MiniAudio/demo_waveform.html). +* [Synthesis Demo](https://www.moros1138.com/demos/olcPGEX_MiniAudio/demo_synthesis.html). # This is an olc::PixelGameEngine Extension @@ -47,9 +50,20 @@ Cross-Platform, out-of-the-box. Easily use in your Linux, Windows, MacOS, and Em * Get the current position in the sample, in milliseconds. * Get the current position in the sample, as float 0.0f is start, 1.0f is end. +### Waveform Features +* Create sine, square, sawtooth, and triangle waves. +* Load and play multiple waveform channels at the same time. +* Modify waveform amplitudes, frequencies, and types in realtime. + +### Noise Generation Features +* Set a callback function to send and play raw audio data for potential sound synthesis. +* Send raw data for both left and right stereo channels. +* Track passage of audio frame time for oscillators / time-sensitive applications. + *** Advanced Features, for those who want to use more of miniaudio * Get a pointer to the ma_device -* Get a poitner to the ma_engine +* Get a pointer to the ma_engine +* Get pointers to waveforms and sounds # Usage @@ -109,3 +123,5 @@ That's it! # Acknowledgements I'd like to give a special thanks for JavidX9 (aka OneLoneCoder), AniCator, JustinRichardsMusic, and everybody else who was a part of that audiophile conversation when I asked for help! Your patience and feedback made this project possible. Thank you! + +I'd also like to single out sigonasr2 (Dense Dance 2π) for the waveform functionality and for crafting the demos for them! diff --git a/demo/Makefile b/demo/Makefile index d60f8f9..f5b4b70 100644 --- a/demo/Makefile +++ b/demo/Makefile @@ -1,6 +1,6 @@ INCLUDE := -I../ -I../third_party/miniaudio -I../third_party/olcPixelGameEngine -GCC_FLAGS := -std=c++17 -O2 -lX11 -lGL -lpthread -lpng -lstdc++fs -std=c++17 -ldl -lm -EM_FLAGS := -std=c++17 -O2 -s ALLOW_MEMORY_GROWTH=1 -s MAX_WEBGL_VERSION=2 -s MIN_WEBGL_VERSION=2 -s USE_LIBPNG=1 +GCC_FLAGS := -std=c++20 -O2 -lX11 -lGL -lpthread -lpng -lstdc++fs -ldl -lm +EM_FLAGS := -std=c++20 -O2 -s ALLOW_MEMORY_GROWTH=1 -s MAX_WEBGL_VERSION=2 -s MIN_WEBGL_VERSION=2 -s USE_LIBPNG=1 --shell-file shell.html --preload-file=./assets@assets .PHONY: all linux emscripten clean @@ -9,12 +9,39 @@ all: linux emscripten demo: demo.cpp g++ demo.cpp -o demo ${INCLUDE} ${GCC_FLAGS} +demo_synthesis: demo_synthesis.cpp + g++ demo_synthesis.cpp -o demo_synthesis ${INCLUDE} ${GCC_FLAGS} + +demo_waveform: demo_waveform.cpp + g++ demo_waveform.cpp -o demo_waveform ${INCLUDE} ${GCC_FLAGS} + demo.html: demo.cpp - em++ demo.cpp -o demo.html ${INCLUDE} ${EM_FLAGS} --preload-file=./assets@assets + em++ demo.cpp -o demo.html ${INCLUDE} ${EM_FLAGS} + +demo_synthesis.html: demo_synthesis.cpp + em++ demo_synthesis.cpp -o demo_synthesis.html ${INCLUDE} ${EM_FLAGS} + +demo_waveform.html: demo.cpp + em++ demo_waveform.cpp -o demo_waveform.html ${INCLUDE} ${EM_FLAGS} -linux: demo +linux: demo demo_synthesis demo_waveform -emscripten: demo.html +emscripten: demo.html demo_synthesis.html demo_waveform.html clean: - @rm -f demo demo.html demo.js demo.wasm demo.data + @rm -f \ + demo \ + demo.html \ + demo.js \ + demo.wasm \ + demo.data \ + demo_synthesis \ + demo_synthesis.html \ + demo_synthesis.js \ + demo_synthesis.wasm \ + demo_synthesis.data \ + demo_waveform \ + demo_waveform.html \ + demo_waveform.js \ + demo_waveform.wasm \ + demo_waveform.data diff --git a/demo/demo_synthesis.cpp b/demo/demo_synthesis.cpp new file mode 100644 index 0000000..8acdb69 --- /dev/null +++ b/demo/demo_synthesis.cpp @@ -0,0 +1,479 @@ +#define OLC_PGE_APPLICATION +#include "olcPixelGameEngine.h" + +#define OLC_PGEX_MINIAUDIO +#include "olcPGEX_MiniAudio.h" + +constexpr int NOTE_COUNT = 17; +constexpr float thirtyFramesPerSecond = 1.0f / 30.f; +constexpr float PI = 3.14159f; + +class DemoSynthesis : public olc::PixelGameEngine +{ + // NOTICE + // + // This example is highly inspired by the Sound Synthesizer Part 3 series created by javidx9! + // Checkout the original at https://github.com/OneLoneCoder/synth/blob/master/main3a.cpp + // and the accompanying video at https://www.youtube.com/watch?v=kDuvruJTjOs + // + +public: + DemoSynthesis() + { + sAppName = "Demo MiniAudio - Custom Audio Synthesis"; + } + +public: + + bool OnUserCreate() override + { + instruments.emplace_back(std::make_unique()); + instruments.emplace_back(std::make_unique()); + instruments.emplace_back(std::make_unique()); + instruments.emplace_back(std::make_unique()); + + // This is it... This is how we interface into the miniaudio engine any noise we want heard! + // Fill up the noiseLeftChannel and noiseRightChannel with a value at any given moment... + // Accumulate fElapsedTime so that we know how much total time has passed so we can play the correct synthesized sound based on delta time. + noiseCallbackFunc = [this](float& noiseLeftChannel, float& noiseRightChannel, const float fElapsedTime)->void{ + noiseLeftChannel = noiseRightChannel = 0.f; + + for(Note& note : notes) + { + bool noteFinished{false}; + float noise = note.instrument->sound(audioRuntime, note, noteFinished); + + noiseLeftChannel += noise * lerp(1.f, 0.f, pan/2 + 0.5f); // This lerp formula shrinks the -1 to 1 range to 0 to 1, which determines percentage of the total noise to play in each side. + noiseRightChannel += noise * lerp(0.f, 1.f, pan/2 + 0.5f); + } + + // Each frame the callback receives will give us a duration that frame takes up in real time. + audioRuntime += fElapsedTime; + }; + + // And finally... Tell the system this is where to send audio requests to... Now we can modify audio! + ma.SetNoiseCallback(noiseCallbackFunc); + + //Initialize an instrument for all the notes. + for(Note& note : notes) + { + note.instrument = instruments[selectedInstrumentInd].get(); + } + + return true; + } + + bool OnUserUpdate(float fElapsedTime) override + { + fElapsedTime = (fElapsedTime > thirtyFramesPerSecond) ? thirtyFramesPerSecond : fElapsedTime; + + olc::Pixel backgroundCol{olc::VERY_DARK_GREY}; + + bool keyPressed{false}; + + for(Note& note : notes) + { + note.instrument->volume = 0.1f; + + if(GetKey(note.key).bHeld) + { + // Note is not active yet, so we will play it now. + if(!note.active) + { + note.on = audioRuntime; + note.active=true; + } + else + if(note.off > note.on) // We pressed the key during its release phase... + note.on = audioRuntime; + + keyPressed = true; + } + else + { + if(note.off < note.on) + { + note.off = audioRuntime; + } + } + } + + if(GetKey(olc::UP).bPressed) + selectedInstrumentInd = (selectedInstrumentInd + 1) % instruments.size(); + + if(GetKey(olc::DOWN).bPressed) + { + selectedInstrumentInd--; + while(selectedInstrumentInd < 0) + selectedInstrumentInd += instruments.size(); + } + + if(GetKey(olc::RIGHT).bHeld) + volume = std::min(1.f, volume + 1.f * fElapsedTime); + if(GetKey(olc::LEFT).bHeld) + volume = std::max(0.f, volume - 1.f * fElapsedTime); + + if(GetKey(olc::O).bHeld) + pan = std::max(-1.f, pan - 1.f * fElapsedTime); + if(GetKey(olc::P).bHeld) + pan = std::min(1.f, pan + 1.f * fElapsedTime); + + if(GetKey(olc::R).bPressed) + { + selectedInstrumentInd = 0; + volume = 0.1f; + pan = 0.f; + } + + // Update all the notes' instruments. + for(Note& note : notes) + { + note.instrument = instruments[selectedInstrumentInd].get(); + } + + instruments[selectedInstrumentInd]->volume = volume; + + if(keyPressed) + backgroundCol = olc::VERY_DARK_BLUE; + + GradientFillRectDecal({}, {float(ScreenWidth()), ScreenHeight()/2.f}, olc::BLACK, backgroundCol, backgroundCol, olc::BLACK); + GradientFillRectDecal({0.f, ScreenHeight()/2.f}, {float(ScreenWidth()), ScreenHeight()/2.f}, backgroundCol, olc::BLACK, olc::BLACK, backgroundCol); + + DrawStringDecal({},"Instrument: <"+ instruments[selectedInstrumentInd]->name +"> UP, DOWN"); + DrawStringDecal({0.f, 8.f},"Volume: <"+ std::to_string(volume) +"> LEFT, RIGHT"); + DrawStringDecal({0.f, 16.f},"Pan: <"+ std::to_string(pan) +"> O, P"); + + DrawStringDecal({0.f, 120.f},"Reset Settings "); + + olc::vf2d pianoStrSize{GetTextSize(piano)}; + DrawRotatedStringDecal({ScreenWidth()/2.f, ScreenHeight() - pianoStrSize.y/2}, piano, 0.f, pianoStrSize/2, olc::WHITE, {0.65f, 1.f}); + + #if defined(__EMSCRIPTEN__) + return true; + #else + return !GetKey(olc::ESCAPE).bPressed; + #endif + } + + // The instance of the audio engine, no fancy config required. + olc::MiniAudio ma; + + std::function noiseCallbackFunc; + + double audioRuntime{}; // A running timer of how long the audio engine has been running. + + float volume{0.1f}; + float pan{0.0f}; // -1.f for only left channel, 1.f for only right channel + + struct Instrument; + + struct Note + { + std::string displayStr; + olc::Key key; + int id; // Position in scale + float on; // Time note was activated + float off; // Time note was deactivated + bool active; + Instrument*instrument; + + Note(std::string displayStr, olc::Key key, int id) + :displayStr(displayStr), key(key), id(id){ + on = 0.0; + off = 0.0; + active = false; + } + }; + + std::arraynotes{ + Note + {"G#", olc::A, -1}, //This is not a typo. javid's original scale formula starts at "A", so this will retrieve the note ID prior to that one. + {"A" , olc::Z, 0}, + {"A#", olc::S, 1}, + {"B" , olc::X, 2}, + {"C" , olc::C, 3}, + {"C#", olc::F, 4}, + {"D" , olc::V, 5}, + {"D#", olc::G, 6}, + {"E" , olc::B, 7}, + {"F" , olc::N, 8}, + {"F#", olc::J, 9}, + {"G" , olc::M, 10}, + {"G#", olc::K, 11}, + {"A" , olc::COMMA, 12}, + {"A#", olc::L, 13}, + {"B" , olc::PERIOD, 14}, + {"C" , olc::OEM_2, 15}, + }; + + int selectedInstrumentInd{}; + std::vector>instruments; + + const std::string piano{ + " | | | | | | | | | | | | | | | | |\n" + "A | | S | | | F | | G | | | J | | K | | L | | |\n" + "__| |___| | |___| |___| | |___| |___| |___| | |__\n" + "| | | | | | | | | | |\n" + "| Z | X | C | V | B | N | M | , | . | / |\n" + "|_____|_____|_____|_____|_____|_____|_____|_____|_____|_____|" + }; + + enum class Oscillator + { + SINE, + SQUARE, + TRIANGLE, + SAW_ANALOG, + SAW_DIGITAL, + NOISE, + }; + + float lerp(float n1,float n2,double t){ + return float(n1*(1-t)+n2*t); + } + + // Converts frequency (Hz) to angular velocity + static float w(const float hertz) + { + return hertz * 2.0 * PI; + } + + static float osc(const float time, const float hertz, const Oscillator type = Oscillator::SINE, + const float LFOHertz = 0.0, const float LFOAmplitude = 0.0, float custom = 50.0) + { + + float freq = w(hertz) * time + LFOAmplitude * hertz * (sin(w(LFOHertz) * time)); + + switch (type) + { + case Oscillator::SINE: // Sine wave bewteen -1 and +1 + return sin(freq); + + case Oscillator::SQUARE: // Square wave between -1 and +1 + return sin(freq) > 0 ? 1.0 : -1.0; + + case Oscillator::TRIANGLE: // Triangle wave between -1 and +1 + return asin(sin(freq)) * (2.0 / PI); + + case Oscillator::SAW_ANALOG: // Saw wave (analogue / warm / slow) + { + float dOutput = 0.0; + for (float n = 1.0; n < custom; n++) + dOutput += (sin(n*freq)) / n; + return dOutput * (2.0 / PI); + } + + case Oscillator::SAW_DIGITAL: + return atan(tan(freq)); + + case Oscillator::NOISE: + return 2.0 * ((float)rand() / (float)RAND_MAX) - 1.0; + + default: + return 0.0; + } + } + + //Abstract envelope class + struct Envelope + { + virtual float amplitude(const float time, const float timeOn, const float timeOff) = 0; + }; + + struct EnvelopeADSR : public Envelope + { + float attackTime; + float decayTime; + float sustainAmplitude; + float releaseTime; + float startAmplitude; + + EnvelopeADSR() + { + attackTime = 0.1; + decayTime = 0.1; + sustainAmplitude = 1.0; + releaseTime = 0.2; + startAmplitude = 1.0; + } + + virtual float amplitude(const float time, const float timeOn, const float timeOff) + { + float dAmplitude = 0.0; + float dReleaseAmplitude = 0.0; + + if (timeOn > timeOff) // Note is on + { + float dLifeTime = time - timeOn; + + if (dLifeTime <= attackTime) + dAmplitude = (dLifeTime / attackTime) * startAmplitude; + + if (dLifeTime > attackTime && dLifeTime <= (attackTime + decayTime)) + dAmplitude = ((dLifeTime - attackTime) / decayTime) * (sustainAmplitude - startAmplitude) + startAmplitude; + + if (dLifeTime > (attackTime + decayTime)) + dAmplitude = sustainAmplitude; + } + else // Note is off + { + float dLifeTime = timeOff - timeOn; + + if (dLifeTime <= attackTime) + dReleaseAmplitude = (dLifeTime / attackTime) * startAmplitude; + + if (dLifeTime > attackTime && dLifeTime <= (attackTime + decayTime)) + dReleaseAmplitude = ((dLifeTime - attackTime) / decayTime) * (sustainAmplitude - startAmplitude) + startAmplitude; + + if (dLifeTime > (attackTime + decayTime)) + dReleaseAmplitude = sustainAmplitude; + + dAmplitude = ((time - timeOff) / releaseTime) * (0.0 - dReleaseAmplitude) + dReleaseAmplitude; + } + + // Amplitude should not be negative + if (dAmplitude <= 0.000) + dAmplitude = 0.0; + + return dAmplitude; + } + }; + + static float scale(const int noteID) + { + return 256 * pow(1.0594630943592952645618252949463, noteID); + } + + static float envelope(const float time, Envelope &env, const float timeOn, const float timeOff) + { + return env.amplitude(time, timeOn, timeOff); + } + + //An abstract struct for other instruments + struct Instrument + { + float volume; + std::string name; + EnvelopeADSR env; + virtual float sound(const float time, Note&n, bool &bNoteFinished) = 0; + }; + + struct Bell : public Instrument + { + Bell() + { + env.attackTime = 0.01; + env.decayTime = 1.0; + env.sustainAmplitude = 0.0; + env.releaseTime = 1.0; + + volume = 1.0; + name = "Bell"; + } + + virtual float sound(const float time, Note&n, bool ¬eFinished) override + { + float amplitude = envelope(time, env, n.on, n.off); + if (amplitude <= 0.0) noteFinished = true; + + float sound = + + 1.00 * osc(n.on - time, scale(n.id + 12), Oscillator::SINE, 5.0, 0.001) + + 0.50 * osc(n.on - time, scale(n.id + 24)) + + 0.25 * osc(n.on - time, scale(n.id + 36)); + + return amplitude * sound * volume; + } + + }; + + struct Bell8 : public Instrument + { + Bell8() + { + env.attackTime = 0.01; + env.decayTime = 0.5; + env.sustainAmplitude = 0.8; + env.releaseTime = 1.0; + + volume = 1.0; + name = "Bell 8-bit"; + } + + virtual float sound(const float time, Note&n, bool ¬eFinished) override + { + float amplitude = envelope(time, env, n.on, n.off); + if (amplitude <= 0.0) noteFinished = true; + + float sound = + +1.00 * osc(n.on - time, scale(n.id), Oscillator::SQUARE, 5.0, 0.001) + + 0.50 * osc(n.on - time, scale(n.id + 12)) + + 0.25 * osc(n.on - time, scale(n.id + 24)); + + return amplitude * sound * volume; + } + + }; + + struct Harmonica : public Instrument + { + Harmonica() + { + env.attackTime = 0.05; + env.decayTime = 1.0; + env.sustainAmplitude = 0.95; + env.releaseTime = 0.1; + + volume = 1.0; + name = "Harmonica"; + } + + virtual float sound(const float time, Note&n, bool ¬eFinished) override + { + float amplitude = envelope(time, env, n.on, n.off); + if (amplitude <= 0.0) noteFinished = true; + + float sound = + //+ 1.0 * synth::osc(n.on - dTime, synth::scale(n.id-12), synth::OSC_SAW_ANA, 5.0, 0.001, 100) + + 1.00 * osc(n.on - time, scale(n.id), Oscillator::SQUARE, 5.0, 0.001) + + 0.50 * osc(n.on - time, scale(n.id + 12), Oscillator::SQUARE) + + 0.05 * osc(n.on - time, scale(n.id + 24), Oscillator::NOISE); + + return amplitude * sound * volume; + } + + }; + + struct SawAnalog : public Instrument + { + SawAnalog() + { + env.attackTime = 0.05; + env.decayTime = 1.0; + env.sustainAmplitude = 0.95; + env.releaseTime = 0.1; + + volume = 1.0; + name = "Analog Saw"; + } + + virtual float sound(const float time, Note&n, bool ¬eFinished) override + { + float amplitude = envelope(time, env, n.on, n.off); + if (amplitude <= 0.0) noteFinished = true; + + float sound = + + 1.00 * osc(n.on - time, scale(n.id), Oscillator::SAW_ANALOG, 5.0, 0.001); + + return amplitude * sound * volume; + } + + }; +}; + +int main() +{ + DemoSynthesis demo; + if (demo.Construct(320, 180, 4, 4)) + demo.Start(); + return 0; +} diff --git a/demo/demo_waveform.cpp b/demo/demo_waveform.cpp new file mode 100644 index 0000000..ffc2399 --- /dev/null +++ b/demo/demo_waveform.cpp @@ -0,0 +1,187 @@ +#define OLC_PGE_APPLICATION +#include "olcPixelGameEngine.h" + +#define OLC_PGEX_MINIAUDIO +#include "olcPGEX_MiniAudio.h" + +constexpr int NOTE_COUNT = 17; +constexpr float thirtyFramesPerSecond = 1.0f / 30.f; + +class DemoPiano : public olc::PixelGameEngine +{ + // NOTICE + // + // This example is highly inspired by the Sound Synthesizer Part 1 series created by javidx9! + // Checkout the original at https://github.com/OneLoneCoder/synth/blob/master/main1.cpp + // and the accompanying video at https://www.youtube.com/watch?v=OSCzKOqtgcA + // + + using NoteName = std::string; + using Frequency = float; + +public: + DemoPiano() + { + sAppName = "Demo MiniAudio - Waveforms"; + } + +public: + + bool OnUserCreate() override + { + + for (Note¬e : notes) + { + // When calling CreateWaveform, you'll be given a unique ID used in all other Waveform functions. Store it somewhere. + note.waveformId = ma.CreateWaveform(amplitude, note.frequency, selectedWaveform); + } + + return true; + } + + bool OnUserUpdate(float fElapsedTime) override + { + fElapsedTime = (fElapsedTime > thirtyFramesPerSecond) ? thirtyFramesPerSecond : fElapsedTime; + + olc::Pixel backgroundCol{olc::VERY_DARK_GREY}; + + bool keyPressed{false}; + + if(GetKey(olc::Q).bPressed) + { + selectedWaveform = ma_waveform_type((selectedWaveform+1)%4); + } + if(GetKey(olc::UP).bPressed) + { + amplitude = std::min(1.f, amplitude + 0.1f); + } + if(GetKey(olc::DOWN).bPressed) + { + amplitude = std::max(0.f, amplitude - 0.1f); + } + + for (Note¬e : notes) + { + if(GetKey(note.key).bPressed) + { + ma.PlayWaveform(note.waveformId); + } + if(GetKey(note.key).bReleased) + { + ma.StopWaveform(note.waveformId); + } + if(GetKey(note.key).bHeld) + { + keyPressed=true; + } + ma.SetWaveformType(note.waveformId, selectedWaveform); + ma.SetWaveformAmplitude(note.waveformId, amplitude); + } + + if(keyPressed) + backgroundCol = olc::VERY_DARK_BLUE; + + GradientFillRectDecal({}, {float(ScreenWidth()), ScreenHeight()/2.f}, olc::BLACK, backgroundCol, backgroundCol, olc::BLACK); + GradientFillRectDecal({0.f, ScreenHeight()/2.f}, {float(ScreenWidth()), ScreenHeight()/2.f}, backgroundCol, olc::BLACK, olc::BLACK, backgroundCol); + + DrawStringDecal({}, "Waveform Type < " + waveformToName.at(selectedWaveform) + " > Q"); + std::stringstream s; + s << std::fixed << std::setprecision(1) << amplitude; + DrawStringDecal({0,8}, "Amplitude < " + s.str() + " > UP, DOWN"); + + for (float drawY{24}; Note¬e : notes) + { + if(!ma.IsWaveformPlaying(note.waveformId)) + continue; + + if(drawY == 24) + DrawStringDecal({0.f, drawY - 8.f},"Playing: "); + + std::stringstream notePlayingStr; + notePlayingStr << std::setw(5) << note.displayName << std::setw(7) << std::fixed << std::setprecision(2) << note.frequency; + DrawStringDecal({0.f, drawY}, " "+ notePlayingStr.str()); + drawY += 8; + } + + olc::vf2d pianoStrSize{GetTextSize(piano)}; + DrawRotatedStringDecal({ScreenWidth()/2.f, 128.f}, piano, 0.f, pianoStrSize/2, olc::WHITE, {0.65f, 1.f}); + + #if defined(__EMSCRIPTEN__) + return true; + #else + return !GetKey(olc::ESCAPE).bPressed; + #endif + } + bool OnUserDestroy() override + { + + for (Note¬e : notes) + { + //Let's be nice and cleanup after ourselves... + ma.UnloadWaveform(note.waveformId); + } + + return true; + } + + // The instance of the audio engine, no fancy config required. + olc::MiniAudio ma; + + ma_waveform_type selectedWaveform{ma_waveform_type_sine}; + float amplitude{0.1f}; + + struct Note + { + std::string displayName; + float frequency; + olc::Key key; + int waveformId; + }; + + std::arraynotes{ + Note + {"G#", 207.65f, olc::A}, + {"A", 220.00f, olc::Z}, + {"A#", 233.08f, olc::S}, + {"B", 246.94f, olc::X}, + {"C", 261.63f, olc::C}, + {"C#", 277.18f, olc::F}, + {"D", 293.66f, olc::V}, + {"D#", 311.13f, olc::G}, + {"E", 329.63f, olc::B}, + {"F", 349.23f, olc::N}, + {"F#", 369.99f, olc::J}, + {"G", 392.00f, olc::M}, + {"G#", 415.30f, olc::K}, + {"A", 440.00f, olc::COMMA}, + {"A#", 466.16f, olc::L}, + {"B", 493.88f, olc::PERIOD}, + {"C", 523.25f, olc::OEM_2}, + }; + + const std::unordered_mapwaveformToName + { + {ma_waveform_type_sine, "SINE"}, + {ma_waveform_type_square, "SQUARE"}, + {ma_waveform_type_triangle, "TRIANGLE"}, + {ma_waveform_type_sawtooth, "SAWTOOTH"}, + }; + + const std::string piano{ + " | | | | | | | | | | | | | | | | |\n" + "A | | S | | | F | | G | | | J | | K | | L | | |\n" + "__| |___| | |___| |___| | |___| |___| |___| | |__\n" + "| | | | | | | | | | |\n" + "| Z | X | C | V | B | N | M | , | . | / |\n" + "|_____|_____|_____|_____|_____|_____|_____|_____|_____|_____|" + }; + +}; + +int main() +{ + DemoPiano demo; + if (demo.Construct(320, 180, 4, 4)) + demo.Start(); + return 0; +} diff --git a/demo/shell.html b/demo/shell.html new file mode 100644 index 0000000..77fcc29 --- /dev/null +++ b/demo/shell.html @@ -0,0 +1,335 @@ + + + + + + olcPGEX_MiniAudio Demo + + + + + + +
+ +
+ +
+ +
+

+ +

+
+
+ +
+
+ + + + {{{ SCRIPT }}} + + diff --git a/olcPGEX_MiniAudio.h b/olcPGEX_MiniAudio.h index 7f71a39..8c5cf60 100644 --- a/olcPGEX_MiniAudio.h +++ b/olcPGEX_MiniAudio.h @@ -4,7 +4,7 @@ +-------------------------------------------------------------+ | OneLoneCoder Pixel Game Engine Extension | - | MiniAudio v1.6 | + | MiniAudio v1.7 | +-------------------------------------------------------------+ NOTE: UNDER ACTIVE DEVELOPMENT - THERE MAY BE BUGS/GLITCHES @@ -68,7 +68,7 @@ namespace olc class MiniAudio : public olc::PGEX { public: - std::string name = "olcPGEX_MiniAudio v1.6"; + std::string name = "olcPGEX_MiniAudio v1.7"; public: MiniAudio(); @@ -122,6 +122,45 @@ namespace olc unsigned long long GetCursorMilliseconds(const int id); // gets the current position in the sound, as a float between 0.0f and 1.0f float GetCursorFloat(const int id); + + + public: // WAVEFORM AND NOISE GENERATION + struct Waveform; + // creates a new waveform and returns the id of the waveform + const int CreateWaveform(const double amplitude, const double frequency, const ma_waveform_type waveformType); + // starts playing a waveform, continues producing sound until stopped + void PlayWaveform(const int id); + // change the amplitude of a waveform (loudness) + void SetWaveformAmplitude(const int id, const double amplitude); + // change the frequency of a waveform (pitch) + void SetWaveformFrequency(const int id, const double frequency); + // change the type of a waveform + void SetWaveformType(const int id, const ma_waveform_type waveformType); + // stop a waveform from playing + void StopWaveform(const int id); + // unload and free resources of a given waveform + void UnloadWaveform(const int id); + + // getters + // whether or not a waveform is currently playing + const bool IsWaveformPlaying(const int id) const; + // returns waveform amplitude + const double& GetWaveformAmplitude(const int id) const; + // returns waveform frequency + const double& GetWaveformFrequency(const int id) const; + // returns waveform type + const ma_waveform_type& GetWaveformType(const int id) const; + // ADVANCED USAGE, retrieval of raw ma_waveform object + ma_waveform* GetWaveform(const int id) const; + + // noise generation + // set a noise callback function so your application can send sound updates. + // the callback provides two floating point values to override, one for the left channel and one for the right channel. fill them with raw audio data. + // for periodic functions, you can reference the fElapsedTime variable to track how much time passed this frame. Accumulate it somewhere to keep track of the total audio time. + // if you do not change the output channels, the values previously used will be played. + void SetNoiseCallback(std::functioncallbackFunc); + // clears the noise callback and resets the channel values to 0.0 + void ClearNoiseCallback(); public: // ADVANCED FEATURES for those who want to use more of miniaudio // gets the currently loaded persistent sounds @@ -133,6 +172,16 @@ namespace olc // gets a pointer to the ma_engine ma_engine* GetEngine(); + struct Waveform{ + friend class MiniAudio; + bool isPlaying{false}; + Waveform(const double amplitude, const double frequency, const ma_waveform_type waveformType, const ma_device&device); + private: + bool unloaded{false}; + ma_waveform waveform; + ma_waveform_config waveformConfig; + }; + private: /* @@ -153,6 +202,12 @@ namespace olc // this is where the sounds are kept std::vector vecSounds; std::vector vecOneOffSounds; + + static std::vector vecWaveforms; + + static float noiseLeftChannel; + static float noiseRightChannel; + static std::function noiseCallback; }; /** @@ -194,6 +249,13 @@ namespace olc } }; + struct MiniAudioWaveformException : public std::exception + { + const char* what() const throw() + { + return "Failed to initialize a waveform."; + } + }; } @@ -203,6 +265,10 @@ namespace olc namespace olc { bool MiniAudio::backgroundPlay = false; + std::vector MiniAudio::vecWaveforms; + float MiniAudio::noiseLeftChannel{}; + float MiniAudio::noiseRightChannel{}; + std::function MiniAudio::noiseCallback; MiniAudio::MiniAudio() : olc::PGEX(true) { @@ -282,7 +348,91 @@ namespace olc if(!MiniAudio::backgroundPlay && !pge->IsFocused()) return; - ma_engine_read_pcm_frames((ma_engine*)(pDevice->pUserData), pOutput, frameCount, NULL); + /* + The way mixing works is that we just read into a temporary buffer, then take the contents of that buffer and mix it with the + contents of the output buffer by simply adding the samples together. You could also clip the samples to -1..+1, but I'm not + doing that in this example. + */ + + const int CHANNEL_COUNT{2}; + using PlaybackFormat = float; + + ma_result result; + float temp[4096]; + ma_uint32 tempCapInFrames = ma_countof(temp) / CHANNEL_COUNT; + ma_uint32 totalFramesRead = 0; + + while (totalFramesRead < frameCount) { + ma_uint64 iSample; + ma_uint64 framesReadThisIteration{}; + ma_uint32 totalFramesRemaining = frameCount - totalFramesRead; + ma_uint32 framesToReadThisIteration = tempCapInFrames; + if (framesToReadThisIteration > totalFramesRemaining) { + framesToReadThisIteration = totalFramesRemaining; + } + + if(noiseCallback) + { + for(int i = 0; i < frameCount; i++) + { + // we send multiple callbacks into the future to get what sound we should be playing... + // for raw music data from programs like emulators, the user will probably just keep sending the same sound until it changes on their end. + // for periodic functions, they can use the elapsed time this callback sends to accurately determine what + // data should be sent precisely at that moment... + noiseCallback(noiseLeftChannel, noiseRightChannel, 1.f / pDevice->sampleRate); + + // Since we're reading each channel in individually, They have to be interlaced. Each frame we read one value from the left and right channel... + // From the miniaudio documentation + // ******************************** + // A "frame" is one sample for each channel. For example, in a stereo stream (2 channels), one frame is 2 samples: one for the left, one for the right. + // ******************************** + // add to current output. NOTE: we are assuming a channel count of 2!!! + // NOTE: we are assuming a floating point playback format!!! + ((PlaybackFormat*)pOutput)[totalFramesRead + i*CHANNEL_COUNT] += noiseLeftChannel; + ((PlaybackFormat*)pOutput)[totalFramesRead + i*CHANNEL_COUNT + 1] += noiseRightChannel; + } + } + + // read audio data from basic ma engine + result = ma_engine_read_pcm_frames((ma_engine*)(pDevice->pUserData), temp, frameCount, &framesReadThisIteration); + + if (result == MA_SUCCESS && framesReadThisIteration > 0) + { + // add to current output. NOTE: we are assuming a channel count of 2!!! + for (iSample = 0; iSample < framesReadThisIteration*CHANNEL_COUNT; ++iSample) { + // NOTE: we are assuming a floating point playback format!!! + ((PlaybackFormat*)pOutput)[totalFramesRead*CHANNEL_COUNT + iSample] += temp[iSample]; + } + } + + for(Waveform&waveform : MiniAudio::vecWaveforms) + { + if(waveform.unloaded) + continue; + + if(waveform.isPlaying) + { + // read audio data from a waveform + result = ma_waveform_read_pcm_frames(&waveform.waveform, temp, frameCount, &framesReadThisIteration); + if (result != MA_SUCCESS || framesReadThisIteration == 0) { + continue; + } + else + { + // splice it together with other audio data + for (iSample = 0; iSample < framesReadThisIteration*CHANNEL_COUNT; ++iSample) { + ((float*)pOutput)[totalFramesRead*CHANNEL_COUNT + iSample] += temp[iSample]; + } + } + } + } + + totalFramesRead += (ma_uint32)framesReadThisIteration; + + if (framesReadThisIteration < (ma_uint32)framesToReadThisIteration) { + break; /* Reached EOF. */ + } + } } void MiniAudio::SetBackgroundPlay(bool state) @@ -472,6 +622,100 @@ namespace olc return &engine; } + const int MiniAudio::CreateWaveform(const double amplitude, const double frequency, const ma_waveform_type waveformType) + { + // attempt to re-use an empty slot + for(int i = 0; i < vecWaveforms.size(); i++) + { + if(vecWaveforms.at(i).unloaded) + { + vecWaveforms.at(i) = Waveform{amplitude, frequency, waveformType, device}; + return i; + } + } + + // no empty slots, make more room! + const int id = vecWaveforms.size(); + vecWaveforms.emplace_back(amplitude, frequency, waveformType, device); + + return id; + } + + void MiniAudio::PlayWaveform(const int id) + { + vecWaveforms.at(id).isPlaying = true; + } + + void MiniAudio::SetWaveformAmplitude(const int id, const double amplitude) + { + ma_waveform_set_amplitude(&vecWaveforms.at(id).waveform, amplitude); + } + + void MiniAudio::SetWaveformFrequency(const int id, const double frequency) + { + ma_waveform_set_frequency(&vecWaveforms.at(id).waveform, frequency); + } + + void MiniAudio::SetWaveformType(const int id, const ma_waveform_type waveformType) + { + ma_waveform_set_type(&vecWaveforms.at(id).waveform, waveformType); + } + + void MiniAudio::StopWaveform(const int id) + { + vecWaveforms.at(id).isPlaying = false; + } + + void MiniAudio::UnloadWaveform(const int id) + { + ma_waveform_uninit(&vecWaveforms.at(id).waveform); + vecWaveforms.at(id).unloaded = true; + } + + ma_waveform* MiniAudio::GetWaveform(const int id) const + { + return &vecWaveforms.at(id).waveform; + } + + const bool MiniAudio::IsWaveformPlaying(const int id) const + { + return vecWaveforms.at(id).isPlaying; + } + + const double& MiniAudio::GetWaveformAmplitude(const int id) const + { + return vecWaveforms.at(id).waveform.config.amplitude; + } + + const double& MiniAudio::GetWaveformFrequency(const int id) const + { + return vecWaveforms.at(id).waveform.config.frequency; + } + + const ma_waveform_type&MiniAudio::GetWaveformType(const int id) const + { + return vecWaveforms.at(id).waveform.config.type; + } + + MiniAudio::Waveform::Waveform(const double amplitude, const double frequency, const ma_waveform_type waveformType, const ma_device&device) + { + waveformConfig = ma_waveform_config_init(device.playback.format, device.playback.channels, device.sampleRate, waveformType, amplitude, frequency); + if(ma_waveform_init(&waveformConfig, &waveform) != MA_SUCCESS) + throw MiniAudioWaveformException(); + } + + void MiniAudio::SetNoiseCallback(std::functioncallbackFunc) + { + noiseCallback = callbackFunc; + } + + void MiniAudio::ClearNoiseCallback() + { + MiniAudio::noiseLeftChannel = 0.f; + MiniAudio::noiseRightChannel = 0.f; + noiseCallback = {}; + } + } // olc #endif