commit 97a41f0ca378aeffaa17c79bb3e93ac732c5f3a8 Author: Thor Harald Johansen Date: Fri Apr 7 10:08:39 2023 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87e54c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +main +*.o diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0f60342 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +CPPFLAGS=-std=c++11 -Ofast +LDLIBS=-lstdc++ -lportaudio + +all: main + +clean : + rm -f main *.o + +main : main.o oscillator.o + +main.o: svf.h + +oscillator.o: oscillator.h \ No newline at end of file diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..7113e73 --- /dev/null +++ b/main.cpp @@ -0,0 +1,140 @@ +#include +#include +#include + +#include "portaudio.h" + +#include "oscillator.h" +#include "svf.h" + +using namespace std; + +#define SAMPLE_RATE 48000 + +typedef struct { + Oscillator oscs[8]; + SVFx2 filter; + float time; +} State; + +static int audioCallback( + const void *inputBuffer, + void *outputBuffer, + unsigned long framesPerBuffer, + const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, + void *userData ) { + + State *state = (State*) userData; + float *out = (float*)outputBuffer; + + for(int i = 0; i < framesPerBuffer; i++) { + float value = 0; + for(int j = 0; j < 8; ++j) { + value += state->oscs[j].tick(); + } + value *= 0.125; + value *= 0.5; + state->filter.setFrequency(0.21 + 0.20 * sin(M_PI * state->time)); + value = state->filter.tick(value); + *out++ = value; + *out++ = value; + state->time += 1.0 / SAMPLE_RATE; + + } + return 0; +} + +void handlePAError(PaError err) { + Pa_Terminate(); + cerr << "PortAudio error" << endl; + cerr << "Error number: " << err << endl; + cerr << "Error message: " << Pa_GetErrorText(err) << endl; + exit(err); +} + +static State state; +int main(void) { + for(int i = 0; i < 8; ++i) { + float freq = 440 + i; + state.oscs[i].phaseStep = (freq * 2 * M_PI) / SAMPLE_RATE; + state.oscs[i].phase = PIx2 * (float) rand() / RAND_MAX; + } + state.filter.setMode(SVFx2::MODE_LP); + state.filter.setQ(1); + state.time = 0; + + PaStream *stream; + PaError err; + + err = Pa_Initialize(); + if(err != paNoError) { + handlePAError(err); + } + + PaHostApiIndex deviceCount = Pa_GetDeviceCount(); + cout << "Select audio output device:" << endl; + for(int i = 0; i < deviceCount; ++i) { + const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(i); + if(deviceInfo->maxOutputChannels < 2) { + continue; + } + cout << i << ". " << deviceInfo->name << " (" << Pa_GetHostApiInfo(deviceInfo->hostApi)->name << ")" << endl; + } + string input; + PaHostApiIndex deviceIndex; + for(;;) { + getline(cin, input); + try { + deviceIndex = stoi(input); + if(deviceIndex >= 0 && deviceIndex < deviceCount) { + break; + } + } catch(...) { } + cout << "Invalid selection" << endl; + } + + /* Open an audio I/O stream. */ + PaStreamParameters streamParams; + streamParams.device = deviceIndex; + streamParams.channelCount = 2; + streamParams.sampleFormat = paFloat32; + streamParams.suggestedLatency = 0; + streamParams.hostApiSpecificStreamInfo = NULL; + err = Pa_OpenStream( + &stream, + NULL, /* no input channels */ + &streamParams, /* stereo output */ + SAMPLE_RATE, + 0, /* frames per buffer */ + paNoFlag, + audioCallback, + &state); + if(err != paNoError) { + handlePAError(err); + } + + err = Pa_StartStream(stream); + if(err != paNoError) { + handlePAError(err); + } + + const PaStreamInfo *streamInfo = Pa_GetStreamInfo(stream); + cout << "Latency: " << round(1000 * streamInfo->outputLatency) << " ms" << endl; + + /* Wait for Return key */ + getchar(); + + err = Pa_StopStream(stream); + if(err != paNoError) { + handlePAError(err); + } + err = Pa_CloseStream(stream); + if(err != paNoError) { + handlePAError(err); + } + Pa_Terminate(); + cout << "Terminating" << endl; + + return err; +} \ No newline at end of file diff --git a/oscillator.cpp b/oscillator.cpp new file mode 100644 index 0000000..b3c6c05 --- /dev/null +++ b/oscillator.cpp @@ -0,0 +1,8 @@ +#include "oscillator.h" + +Oscillator::Oscillator() { + mode = MODE_SAW; + phase = 0; + phaseStep = (440 * 2 * M_PI) / 44100; + value = 0; +} \ No newline at end of file diff --git a/oscillator.h b/oscillator.h new file mode 100644 index 0000000..a7bc4c3 --- /dev/null +++ b/oscillator.h @@ -0,0 +1,70 @@ +/* PolyBLEP oscillator inspired by: https://www.metafunction.co.uk/post/all-about-digital-oscillators-part-2-blits-bleps + * Further info about BLEP at: https://pbat.ch/sndkit/blep/ + */ + +#include + +#define PIx2 (2 * M_PI) + +class Oscillator { +public: + enum Mode { MODE_SINE, MODE_SAW, MODE_SQUARE }; + + Mode mode; + float phase; + float phaseStep; + float value; + + Oscillator(); + + float polyBlep(float t) { + double dt = phaseStep / PIx2; + + // t-t^2/2 +1/2 + // 0 < t <= 1 + // discontinuities between 0 & 1 + if (t < dt) { + t /= dt; + return t + t - t * t - 1.0; + } + + // t^2/2 +t +1/2 + // -1 <= t <= 0 + // discontinuities between -1 & 0 + else if (t > 1.0 - dt) { + t = (t - 1.0) / dt; + return t * t + t + t + 1.0; + } + + // no discontinuities + // 0 otherwise + else return 0.0; + } + + // This class provides a band-limited oscillator + float tick() { + float t = phase / PIx2; // Define half phase + + if (mode == MODE_SINE) { + value = sin(phase); // No harmonics in sine so no aliasing!! No Poly BLEPs needed! + } else if (mode == MODE_SAW) { + value = (2.0 * phase / PIx2) - 1.0; // Render naive waveshape + value -= polyBlep(t); // Layer output of Poly BLEP on top + } else if (mode == MODE_SQUARE) { + if (phase < M_PI) { + value = 1.0; // Flip + } else { + value = -1.0; // Flop + } + value += polyBlep(t); // Layer output of Poly BLEP on top (flip) + value -= polyBlep(fmod(t + 0.5, 1.0)); // Layer output of Poly BLEP on top (flop) + } + + phase += phaseStep; + if(phase >= PIx2) { + phase -= PIx2; + } + + return value; // Output + } +}; \ No newline at end of file diff --git a/svf.h b/svf.h new file mode 100644 index 0000000..1088a8b --- /dev/null +++ b/svf.h @@ -0,0 +1,107 @@ +/* CEM3320/Oberheim/Phrophet-style filter as described here: https://arxiv.org/pdf/2111.05592.pdf */ + +#include + +inline float clamp(float x, float a, float b) { + return fminf(b, fmaxf(a, x)); +} + +class SVF { +protected: + float as1, as2; + float kdiv; + float kK, kQ; + + inline void updateCoeffs() { + kdiv = 1 + kK/kQ + kK*kK; + } + +public: + typedef struct { + float hp; + float bp; + float lp; + float br; + float ap; + } Output; + + SVF() { + as1 = 0; + as2 = 0; + kK = tan(M_PI_4); + kQ = M_SQRT1_2; + updateCoeffs(); + } + + void setFrequency(float f) { + kK = tan(M_PI_2 * clamp(f, 0, 1)); + updateCoeffs(); + } + + void setQ(float q) { + kQ = q; + updateCoeffs(); + } + + Output tick(float as) { + Output out; + out.hp = (as - (1/kQ + kK) * as1 - as2) / kdiv; + float au = out.hp * kK; + out.bp = au + as1; + as1 = au + out.bp; + au = out.bp * kK; + out.lp = au + as2; + as2 = au + out.lp; + float abr = out.hp + out.lp; + float aap = out.hp+out.lp + (1/kQ) * out.bp; + return out; + } +}; + +class SVFx2 { +public: + enum Mode { MODE_HP, MODE_BP, MODE_LP, MODE_BR, MODE_AP }; + +private: + Mode mode; + SVF first, second; + +public: + SVFx2() { + this->mode = MODE_LP; + } + + void setMode(Mode mode) { + this->mode = mode; + } + + void setFrequency(float f) { + first.setFrequency(f); + second.setFrequency(f); + } + + void setQ(float q) { + first.setQ(q); + second.setQ(q); + } + + float tick(float as) { + SVF::Output in = first.tick(as); + switch(mode) { + case MODE_HP: + return second.tick(in.hp).hp; + + case MODE_BP: + return second.tick(in.bp).bp; + + case MODE_LP: + return second.tick(in.lp).lp; + + case MODE_BR: + return second.tick(in.br).br; + + case MODE_AP: + return second.tick(in.ap).ap; + } + } +}; \ No newline at end of file