diff --git a/Makefile b/Makefile index e97f572c..6026dbc0 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,8 @@ CXX = g++ SOURCES += ext/noc/noc_file_dialog.c CFLAGS += -DNOC_FILE_DIALOG_GTK $(shell pkg-config --cflags gtk+-2.0) CXXFLAGS += -DLINUX -LDFLAGS += -rdynamic -lpthread -lGL -lGLEW -lglfw -ldl -ljansson -lportaudio -lportmidi -lsamplerate \ +LDFLAGS += -rdynamic \ + -lpthread -lGL -lGLEW -lglfw -ldl -ljansson -lportaudio -lportmidi -lsamplerate \ $(shell pkg-config --libs gtk+-2.0) TARGET = Rack endif diff --git a/README.md b/README.md index fdcf383d..78fa32ec 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ *Note: This software is in semi-public alpha. If you have stumbled upon this project, feel free to try it out and report bugs to the GitHub issue tracker. However, with more users it becomes difficult to make breaking changes, so please don't spread this to your friends and the Internet just yet, until the official announcement has been made.* +*Feel free to post **bugs**, **enhancements**, and **questions** on the [Issue Tracker](https://github.com/AndrewBelt/Rack/issues). To vote for a feature, give a thumbs-up on the first post.* + # Rack Open source Eurorack-style modular DAW diff --git a/include/dsp.hpp b/include/dsp.hpp index dda07723..8d1b7fa1 100644 --- a/include/dsp.hpp +++ b/include/dsp.hpp @@ -3,6 +3,11 @@ #include #include #include +#include "util.hpp" + + +// in minBLEP.cpp +float *generateMinBLEP(int zeroCrossings, int overSampling); namespace rack { @@ -28,6 +33,9 @@ struct RingBuffer { T shift() { return data[mask(start++)]; } + void clear() { + start = end; + } bool empty() const { return start >= end; } @@ -61,6 +69,9 @@ struct DoubleRingBuffer { T shift() { return data[mask(start++)]; } + void clear() { + start = end; + } bool empty() const { return start >= end; } @@ -149,13 +160,14 @@ struct AppleRingBuffer { }; +template struct SampleRateConverter { SRC_STATE *state; SRC_DATA data; SampleRateConverter() { int error; - state = src_new(SRC_SINC_FASTEST, 1, &error); + state = src_new(SRC_SINC_FASTEST, CHANNELS, &error); assert(!error); data.src_ratio = 1.0; @@ -165,53 +177,71 @@ struct SampleRateConverter { src_delete(state); } void setRatio(float r) { + src_set_ratio(state, r); data.src_ratio = r; } - void process(float *in, int inLength, float *out, int outLength) { + /** `in` and `out` are interlaced with the number of channels */ + void process(float *in, int *inFrames, float *out, int *outFrames) { data.data_in = in; - data.input_frames = inLength; + data.input_frames = *inFrames; data.data_out = out; - data.output_frames = outLength; + data.output_frames = *outFrames; src_process(state, &data); + *inFrames = data.input_frames_used; + *outFrames = data.output_frames_gen; + } + void reset() { + src_reset(state); } }; -template -struct Decimator { - SRC_STATE *state; - SRC_DATA data; +template +struct MinBLEP { + float buf[2*ZERO_CROSSINGS] = {}; + int pos = 0; + /** You must set this to the array generated by generateMinBLEP() */ + float *minblep = NULL; + int oversample; + + /** Places a discontinuity with magnitude dx at -1 < p <= 0 relative to the current frame */ + void jump(float p, float dx) { + if (p <= -1 || 0 < p) + return; + for (int j = 0; j < 2*ZERO_CROSSINGS; j++) { + float minblepIndex = ((float)j - p) * oversample; + int index = (pos + j) % (2*ZERO_CROSSINGS); + buf[index] += dx * (-1.0 + interpf(minblep, minblepIndex)); + } + } + float shift() { + float v = buf[pos]; + buf[pos] = 0.0; + pos = (pos + 1) % (2*ZERO_CROSSINGS); + return v; + } +}; - Decimator() { - int error; - state = src_new(SRC_SINC_FASTEST, 1, &error); - assert(!error); - data.data_in = NULL; - data.data_out = NULL; - data.input_frames = OVERSAMPLE; - data.output_frames = 1; - data.end_of_input = false; - data.src_ratio = 1.0 / OVERSAMPLE; +struct RCFilter { + float c = 0.0; + float xstate[1] = {}; + float ystate[1] = {}; + + // `r` is the ratio between the cutoff frequency and sample rate, i.e. r = f_c / f_s + void setCutoff(float r) { + c = 2.0 / r; } - ~Decimator() { - src_delete(state); + void process(float x) { + float y = (x + xstate[0] - ystate[0] * (1 - c)) / (1 + c); + xstate[0] = x; + ystate[0] = y; } - /** input must be length OVERSAMPLE */ - float process(float *input) { - float output[1]; - data.data_in = input; - data.data_out = output; - src_process(state, &data); - if (data.output_frames_gen > 0) { - return output[0]; - } - else { - return 0.0; - } + float lowpass() { + return ystate[0]; } - void reset() { - src_reset(state); + float highpass() { + return xstate[0] - ystate[0]; } }; diff --git a/include/math.hpp b/include/math.hpp new file mode 100644 index 00000000..0eebba0a --- /dev/null +++ b/include/math.hpp @@ -0,0 +1,245 @@ +#pragma once + +#include + + +namespace rack { + +/** Limits a value between a minimum and maximum +If min > max for some reason, returns min +*/ +inline float clampf(float x, float min, float max) { + if (x > max) + x = max; + if (x < min) + x = min; + return x; +} + +/** If the magnitude of x if less than eps, return 0 */ +inline float chopf(float x, float eps) { + if (x < eps && x > -eps) + return 0.0; + return x; +} + +inline float mapf(float x, float xMin, float xMax, float yMin, float yMax) { + return yMin + (x - xMin) / (xMax - xMin) * (yMax - yMin); +} + +inline float crossf(float a, float b, float frac) { + return (1.0 - frac) * a + frac * b; +} + +inline int mini(int a, int b) { + return a < b ? a : b; +} + +inline int maxi(int a, int b) { + return a > b ? a : b; +} + +inline float quadraticBipolar(float x) { + float x2 = x*x; + return x >= 0.0 ? x2 : -x2; +} + +inline float cubic(float x) { + // optimal with --fast-math + return x*x*x; +} + +inline float quarticBipolar(float x) { + float x2 = x*x; + float x4 = x2*x2; + return x >= 0.0 ? x4 : -x4; +} + +inline float quintic(float x) { + // optimal with --fast-math + return x*x*x*x*x; +} + +// Euclidean modulus, always returns 0 <= mod < base for positive base +// Assumes this architecture's division is non-Euclidean +inline int eucMod(int a, int base) { + int mod = a % base; + return mod < 0 ? mod + base : mod; +} + +inline float getf(const float *p, float v = 0.0) { + return p ? *p : v; +} + +inline void setf(float *p, float v) { + if (p) + *p = v; +} + +/** Linearly interpolate an array `p` with index `x` +Assumes that the array at `p` is of length at least ceil(x)+1. +*/ +inline float interpf(const float *p, float x) { + int xi = x; + float xf = x - xi; + return crossf(p[xi], p[xi+1], xf); +} + +//////////////////// +// 2D float vector +//////////////////// + +struct Vec { + float x, y; + + Vec() : x(0.0), y(0.0) {} + Vec(float x, float y) : x(x), y(y) {} + + Vec neg() { + return Vec(-x, -y); + } + Vec plus(Vec b) { + return Vec(x + b.x, y + b.y); + } + Vec minus(Vec b) { + return Vec(x - b.x, y - b.y); + } + Vec mult(float s) { + return Vec(x * s, y * s); + } + Vec div(float s) { + return Vec(x / s, y / s); + } + float dot(Vec b) { + return x * b.x + y * b.y; + } + float norm() { + return hypotf(x, y); + } + Vec min(Vec b) { + return Vec(fminf(x, b.x), fminf(y, b.y)); + } + Vec max(Vec b) { + return Vec(fmaxf(x, b.x), fmaxf(y, b.y)); + } + Vec round() { + return Vec(roundf(x), roundf(y)); + } +}; + + +struct Rect { + Vec pos; + Vec size; + + Rect() {} + Rect(Vec pos, Vec size) : pos(pos), size(size) {} + + /** Returns whether this Rect contains another Rect, inclusive on the top/left, non-inclusive on the bottom/right */ + bool contains(Vec v) { + return pos.x <= v.x && v.x < pos.x + size.x + && pos.y <= v.y && v.y < pos.y + size.y; + } + /** Returns whether this Rect overlaps with another Rect */ + bool intersects(Rect r) { + return (pos.x + size.x > r.pos.x && r.pos.x + r.size.x > pos.x) + && (pos.y + size.y > r.pos.y && r.pos.y + r.size.y > pos.y); + } + Vec getCenter() { + return pos.plus(size.mult(0.5)); + } + Vec getTopRight() { + return pos.plus(Vec(size.x, 0.0)); + } + Vec getBottomLeft() { + return pos.plus(Vec(0.0, size.y)); + } + Vec getBottomRight() { + return pos.plus(size); + } + /** Clamps the position to fix inside a bounding box */ + Rect clamp(Rect bound) { + Rect r; + r.size = size; + r.pos.x = clampf(pos.x, bound.pos.x, bound.pos.x + bound.size.x - size.x); + r.pos.y = clampf(pos.y, bound.pos.y, bound.pos.y + bound.size.y - size.y); + return r; + } +}; + +//////////////////// +// Simple FFT implementation +//////////////////// + +// Derived from the Italian Wikipedia article for FFT +// https://it.wikipedia.org/wiki/Trasformata_di_Fourier_veloce +// If you need speed, use KissFFT, pffft, etc instead. + +inline int log2i(int n) { + int i = 0; + while (n >>= 1) { + i++; + } + return i; +} + +inline bool isPowerOf2(int n) { + return n > 0 && (n & (n-1)) == 0; +} + +/* +inline int reverse(int N, int n) //calculating revers number +{ + int j, p = 0; + for(j = 1; j <= log2i(N); j++) { + if(n & (1 << (log2i(N) - j))) + p |= 1 << (j - 1); + } + return p; +} + +inline void ordina(complex* f1, int N) //using the reverse order in the array +{ + complex f2[MAX]; + for(int i = 0; i < N; i++) + f2[i] = f1[reverse(N, i)]; + for(int j = 0; j < N; j++) + f1[j] = f2[j]; +} + +inline void transform(complex* f, int N) +{ + ordina(f, N); //first: reverse order + complex *W; + W = (complex *)malloc(N / 2 * sizeof(complex)); + W[1] = polar(1., -2. * M_PI / N); + W[0] = 1; + for(int i = 2; i < N / 2; i++) + W[i] = pow(W[1], i); + int n = 1; + int a = N / 2; + for(int j = 0; j < log2i(N); j++) { + for(int i = 0; i < N; i++) { + if(!(i & n)) { + complex temp = f[i]; + complex Temp = W[(i * a) % (n * a)] * f[i + n]; + f[i] = temp + Temp; + f[i + n] = temp - Temp; + } + } + n *= 2; + a = a / 2; + } +} + +inline void FFT(complex* f, int N, double d) +{ + transform(f, N); + for(int i = 0; i < N; i++) + f[i] *= d; //multiplying by step +} +*/ + + + +} // namespace rack diff --git a/include/rack.hpp b/include/rack.hpp index cc06cfea..d4171ea5 100644 --- a/include/rack.hpp +++ b/include/rack.hpp @@ -95,8 +95,10 @@ struct Wire { int outputId; Module *inputModule = NULL; int inputId; - // The voltage which is pointed to by module inputs/outputs - float value = 0.0; + /** The voltage connected to input ports */ + float inputValue = 0.0; + /** The voltage connected to output ports */ + float outputValue = 0.0; }; struct Rack { diff --git a/include/util.hpp b/include/util.hpp index c19ffccc..48071feb 100644 --- a/include/util.hpp +++ b/include/util.hpp @@ -1,96 +1,12 @@ #pragma once #include -#include -#include #include +#include "math.hpp" namespace rack { -//////////////////// -// Math -//////////////////// - -/** Limits a value between a minimum and maximum -If min > max for some reason, returns min -*/ -inline float clampf(float x, float min, float max) { - if (x > max) - x = max; - if (x < min) - x = min; - return x; -} - -/** If the magnitude of x if less than eps, return 0 */ -inline float chopf(float x, float eps) { - if (x < eps && x > -eps) - return 0.0; - return x; -} - -inline float mapf(float x, float xMin, float xMax, float yMin, float yMax) { - return yMin + (x - xMin) / (xMax - xMin) * (yMax - yMin); -} - -inline float crossf(float a, float b, float frac) { - return (1.0 - frac) * a + frac * b; -} - -inline int mini(int a, int b) { - return a < b ? a : b; -} - -inline int maxi(int a, int b) { - return a > b ? a : b; -} - -inline float quadraticBipolar(float x) { - float x2 = x*x; - return x >= 0.0 ? x2 : -x2; -} - -inline float cubic(float x) { - // optimal with --fast-math - return x*x*x; -} - -inline float quarticBipolar(float x) { - float x2 = x*x; - float x4 = x2*x2; - return x >= 0.0 ? x4 : -x4; -} - -inline float quintic(float x) { - // optimal with --fast-math - return x*x*x*x*x; -} - -// Euclidean modulus, always returns 0 <= mod < base for positive base -// Assumes this architecture's division is non-Euclidean -inline int eucMod(int a, int base) { - int mod = a % base; - return mod < 0 ? mod + base : mod; -} - -inline float getf(const float *p, float v = 0.0) { - return p ? *p : v; -} - -inline void setf(float *p, float v) { - if (p) - *p = v; -} - -/** Linearly interpolate an array `p` with index `x` -Assumes that the array at `p` is of length at least ceil(x)+1. -*/ -inline float interpf(const float *p, float x) { - int xi = x; - float xf = x - xi; - return crossf(p[xi], p[xi+1], xf); -} //////////////////// // RNG @@ -102,88 +18,6 @@ float randomf(); /** Returns a normal random number with mean 0 and std dev 1 */ float randomNormal(); -//////////////////// -// 2D float vector -//////////////////// - -struct Vec { - float x, y; - - Vec() : x(0.0), y(0.0) {} - Vec(float x, float y) : x(x), y(y) {} - - Vec neg() { - return Vec(-x, -y); - } - Vec plus(Vec b) { - return Vec(x + b.x, y + b.y); - } - Vec minus(Vec b) { - return Vec(x - b.x, y - b.y); - } - Vec mult(float s) { - return Vec(x * s, y * s); - } - Vec div(float s) { - return Vec(x / s, y / s); - } - float dot(Vec b) { - return x * b.x + y * b.y; - } - float norm() { - return hypotf(x, y); - } - Vec min(Vec b) { - return Vec(fminf(x, b.x), fminf(y, b.y)); - } - Vec max(Vec b) { - return Vec(fmaxf(x, b.x), fmaxf(y, b.y)); - } - Vec round() { - return Vec(roundf(x), roundf(y)); - } -}; - - -struct Rect { - Vec pos; - Vec size; - - Rect() {} - Rect(Vec pos, Vec size) : pos(pos), size(size) {} - - /** Returns whether this Rect contains another Rect, inclusive on the top/left, non-inclusive on the bottom/right */ - bool contains(Vec v) { - return pos.x <= v.x && v.x < pos.x + size.x - && pos.y <= v.y && v.y < pos.y + size.y; - } - /** Returns whether this Rect overlaps with another Rect */ - bool intersects(Rect r) { - return (pos.x + size.x > r.pos.x && r.pos.x + r.size.x > pos.x) - && (pos.y + size.y > r.pos.y && r.pos.y + r.size.y > pos.y); - } - Vec getCenter() { - return pos.plus(size.mult(0.5)); - } - Vec getTopRight() { - return pos.plus(Vec(size.x, 0.0)); - } - Vec getBottomLeft() { - return pos.plus(Vec(0.0, size.y)); - } - Vec getBottomRight() { - return pos.plus(size); - } - /** Clamps the position to fix inside a bounding box */ - Rect clamp(Rect bound) { - Rect r; - r.size = size; - r.pos.x = clampf(pos.x, bound.pos.x, bound.pos.x + bound.size.x - size.x); - r.pos.y = clampf(pos.y, bound.pos.y, bound.pos.y + bound.size.y - size.y); - return r; - } -}; - //////////////////// // Helper functions //////////////////// @@ -191,5 +25,7 @@ struct Rect { /** Converts a printf format string and optional arguments into a std::string */ std::string stringf(const char *format, ...); +/** Truncates and adds "..." to a string, not exceeding `len` characters */ +std::string ellipsize(std::string s, size_t len); } // namespace rack diff --git a/src/Rack.cpp b/src/Rack.cpp index b14df79f..9dd13838 100644 --- a/src/Rack.cpp +++ b/src/Rack.cpp @@ -132,6 +132,11 @@ void Rack::step() { const float lambda = 1.0; module->cpuTime += (elapsed - module->cpuTime) * lambda / sampleRate; } + // Step cables by moving their output values to inputs + for (Wire *wire : impl->wires) { + wire->inputValue = wire->outputValue; + wire->outputValue = 0.0; + } } void Rack::addModule(Module *module) { @@ -166,8 +171,6 @@ void Rack::addWire(Wire *wire) { assert(wire); VIPLock vipLock(impl->vipMutex); std::lock_guard lock(impl->mutex); - // It would probably be good to reset the wire voltage - wire->value = 0.0; // Check that the wire is not already added assert(impl->wires.find(wire) == impl->wires.end()); assert(wire->outputModule); @@ -180,8 +183,8 @@ void Rack::addWire(Wire *wire) { } // Connect the wire to inputModule impl->wires.insert(wire); - wire->inputModule->inputs[wire->inputId] = &wire->value; - wire->outputModule->outputs[wire->outputId] = &wire->value; + wire->inputModule->inputs[wire->inputId] = &wire->inputValue; + wire->outputModule->outputs[wire->outputId] = &wire->outputValue; } void Rack::removeWire(Wire *wire) { diff --git a/src/core/AudioInterface.cpp b/src/core/AudioInterface.cpp index f21f514c..c9da0e06 100644 --- a/src/core/AudioInterface.cpp +++ b/src/core/AudioInterface.cpp @@ -2,6 +2,7 @@ #include #include #include "core.hpp" +#include "dsp.hpp" using namespace rack; @@ -9,7 +10,7 @@ using namespace rack; void audioInit() { PaError err = Pa_Initialize(); if (err) { - printf("Failed to initialize PortAudio: %s\n", Pa_GetErrorText(err)); + fprintf(stderr, "Failed to initialize PortAudio: %s\n", Pa_GetErrorText(err)); return; } } @@ -22,31 +23,35 @@ struct AudioInterface : Module { enum InputIds { AUDIO1_INPUT, AUDIO2_INPUT, - AUDIO3_INPUT, - AUDIO4_INPUT, NUM_INPUTS }; enum OutputIds { AUDIO1_OUTPUT, AUDIO2_OUTPUT, - AUDIO3_OUTPUT, - AUDIO4_OUTPUT, NUM_OUTPUTS }; PaStream *stream = NULL; - int numOutputs; - int numInputs; - int bufferFrame; - float outputBuffer[1<<14] = {}; - float inputBuffer[1<<14] = {}; - // Used because the GUI thread and Rack thread can both interact with this class - std::mutex mutex; - - // Set these and call openDevice() + // Stream properties int deviceId = -1; float sampleRate = 44100.0; int blockSize = 256; + int numOutputs = 0; + int numInputs = 0; + + // Used because the GUI thread and Rack thread can both interact with this class + std::mutex mutex; + + SampleRateConverter<2> inSrc; + SampleRateConverter<2> outSrc; + + struct Frame { + float samples[2]; + }; + // in device's sample rate + RingBuffer inBuffer; + // in rack's sample rate + RingBuffer outBuffer; AudioInterface(); ~AudioInterface(); @@ -54,8 +59,19 @@ struct AudioInterface : Module { int getDeviceCount(); std::string getDeviceName(int deviceId); - void openDevice(); + + void openDevice(int deviceId, float sampleRate, int blockSize); void closeDevice(); + + void setDeviceId(int deviceId) { + openDevice(deviceId, sampleRate, blockSize); + } + void setSampleRate(float sampleRate) { + openDevice(deviceId, sampleRate, blockSize); + } + void setBlockSize(int blockSize) { + openDevice(deviceId, sampleRate, blockSize); + } }; @@ -63,7 +79,6 @@ AudioInterface::AudioInterface() { params.resize(NUM_PARAMS); inputs.resize(NUM_INPUTS); outputs.resize(NUM_OUTPUTS); - closeDevice(); } AudioInterface::~AudioInterface() { @@ -72,51 +87,97 @@ AudioInterface::~AudioInterface() { void AudioInterface::step() { std::lock_guard lock(mutex); - - if (!stream) { - setf(inputs[AUDIO1_OUTPUT], 0.0); - setf(inputs[AUDIO2_OUTPUT], 0.0); - setf(inputs[AUDIO3_OUTPUT], 0.0); - setf(inputs[AUDIO4_OUTPUT], 0.0); + if (!stream) return; - } - // Input ports -> Output buffer - for (int i = 0; i < numOutputs; i++) { - outputBuffer[numOutputs * bufferFrame + i] = getf(inputs[i]) / 5.0; - } - // Input buffer -> Output ports - for (int i = 0; i < numInputs; i++) { - setf(outputs[i], inputBuffer[numOutputs * bufferFrame + i] * 5.0); + // Get input and pass it through the sample rate converter + if (numOutputs > 0) { + Frame f; + f.samples[0] = getf(inputs[AUDIO1_INPUT]); + f.samples[1] = getf(inputs[AUDIO2_INPUT]); + + inSrc.setRatio(sampleRate / gRack->sampleRate); + int inLength = 1; + int outLength = 16; + float buf[2*outLength]; + inSrc.process(f.samples, &inLength, buf, &outLength); + for (int i = 0; i < outLength; i++) { + if (inBuffer.full()) + break; + Frame f; + f.samples[0] = buf[2*i + 0]; + f.samples[1] = buf[2*i + 1]; + inBuffer.push(f); + } } - if (++bufferFrame >= blockSize) { - bufferFrame = 0; + // Read/write stream if we have enough input + // TODO If numOutputs == 0, call this when outBuffer.empty() + bool streamReady = (numOutputs > 0) ? ((int)inBuffer.size() >= blockSize) : (outBuffer.empty()); + if (streamReady) { + // printf("%d %d\n", inBuffer.size(), outBuffer.size()); PaError err; - // Input + // Read output from input stream // (for some reason, if you write the output stream before you read the input stream, PortAudio can segfault on Windows.) if (numInputs > 0) { - err = Pa_ReadStream(stream, inputBuffer, blockSize); + float *buf = new float[numInputs * blockSize]; + err = Pa_ReadStream(stream, buf, blockSize); if (err) { // Ignore buffer underflows if (err != paInputOverflowed) { - printf("Audio input buffer underflow\n"); + fprintf(stderr, "Audio input buffer underflow\n"); } } + + // Pass output through sample rate converter + outSrc.setRatio(gRack->sampleRate / sampleRate); + int inLength = blockSize; + int outLength = 8*blockSize; + float *outBuf = new float[2*outLength]; + outSrc.process(buf, &inLength, outBuf, &outLength); + // Add to output ring buffer + for (int i = 0; i < outLength; i++) { + if (outBuffer.full()) + break; + Frame f; + f.samples[0] = outBuf[2*i + 0]; + f.samples[1] = outBuf[2*i + 1]; + outBuffer.push(f); + } + delete[] outBuf; + delete[] buf; } - // Output + + // Write input to output stream if (numOutputs > 0) { - err = Pa_WriteStream(stream, outputBuffer, blockSize); + float *buf = new float[numOutputs * blockSize](); + for (int i = 0; i < blockSize; i++) { + assert(!inBuffer.empty()); + Frame f = inBuffer.shift(); + for (int channel = 0; channel < numOutputs; channel++) { + buf[i * numOutputs + channel] = f.samples[channel]; + } + } + + err = Pa_WriteStream(stream, buf, blockSize); if (err) { // Ignore buffer underflows if (err != paOutputUnderflowed) { - printf("Audio output buffer underflow\n"); + fprintf(stderr, "Audio output buffer underflow\n"); } } + delete[] buf; } } + + // Set output + if (!outBuffer.empty()) { + Frame f = outBuffer.shift(); + setf(outputs[AUDIO1_OUTPUT], f.samples[0]); + setf(outputs[AUDIO2_OUTPUT], f.samples[1]); + } } int AudioInterface::getDeviceCount() { @@ -131,52 +192,61 @@ std::string AudioInterface::getDeviceName(int deviceId) { return stringf("%s: %s (%d in, %d out)", apiInfo->name, info->name, info->maxInputChannels, info->maxOutputChannels); } -void AudioInterface::openDevice() { +void AudioInterface::openDevice(int deviceId, float sampleRate, int blockSize) { closeDevice(); std::lock_guard lock(mutex); + this->sampleRate = sampleRate; + this->blockSize = blockSize; + // Open new device if (deviceId >= 0) { PaError err; - const PaDeviceInfo *info = Pa_GetDeviceInfo(deviceId); - if (!info) { - printf("Failed to query audio device\n"); + const PaDeviceInfo *deviceInfo = Pa_GetDeviceInfo(deviceId); + if (!deviceInfo) { + fprintf(stderr, "Failed to query audio device\n"); return; } - numOutputs = mini(info->maxOutputChannels, 4); - numInputs = mini(info->maxInputChannels, 4); + numOutputs = mini(deviceInfo->maxOutputChannels, 2); + numInputs = mini(deviceInfo->maxInputChannels, 2); PaStreamParameters outputParameters; outputParameters.device = deviceId; outputParameters.channelCount = numOutputs; outputParameters.sampleFormat = paFloat32; - outputParameters.suggestedLatency = info->defaultLowOutputLatency; + outputParameters.suggestedLatency = deviceInfo->defaultLowOutputLatency; outputParameters.hostApiSpecificStreamInfo = NULL; PaStreamParameters inputParameters; inputParameters.device = deviceId; inputParameters.channelCount = numInputs; inputParameters.sampleFormat = paFloat32; - inputParameters.suggestedLatency = info->defaultLowInputLatency; + inputParameters.suggestedLatency = deviceInfo->defaultLowInputLatency; inputParameters.hostApiSpecificStreamInfo = NULL; // Don't use stream parameters if 0 input or output channels err = Pa_OpenStream(&stream, - numInputs == 0 ? NULL : &outputParameters, - numOutputs == 0 ? NULL : &inputParameters, + numInputs == 0 ? NULL : &inputParameters, + numOutputs == 0 ? NULL : &outputParameters, sampleRate, blockSize, paNoFlag, NULL, NULL); if (err) { - printf("Failed to open audio stream: %s\n", Pa_GetErrorText(err)); + fprintf(stderr, "Failed to open audio stream: %s\n", Pa_GetErrorText(err)); return; } err = Pa_StartStream(stream); if (err) { - printf("Failed to start audio stream: %s\n", Pa_GetErrorText(err)); + fprintf(stderr, "Failed to start audio stream: %s\n", Pa_GetErrorText(err)); return; } + + // Correct sample rate + const PaStreamInfo *streamInfo = Pa_GetStreamInfo(stream); + this->sampleRate = streamInfo->sampleRate; } + + this->deviceId = deviceId; } void AudioInterface::closeDevice() { @@ -187,13 +257,19 @@ void AudioInterface::closeDevice() { err = Pa_CloseStream(stream); if (err) { // This shouldn't happen: - printf("Failed to close audio stream: %s\n", Pa_GetErrorText(err)); + fprintf(stderr, "Failed to close audio stream: %s\n", Pa_GetErrorText(err)); } - stream = NULL; } + + // Reset stream settings + stream = NULL; + deviceId = -1; numOutputs = 0; numInputs = 0; - bufferFrame = 0; + + // Clear buffers + inBuffer.clear(); + outBuffer.clear(); } @@ -201,8 +277,7 @@ struct AudioItem : MenuItem { AudioInterface *audioInterface; int deviceId; void onAction() { - audioInterface->deviceId = deviceId; - audioInterface->openDevice(); + audioInterface->setDeviceId(deviceId); } }; @@ -233,10 +308,7 @@ struct AudioChoice : ChoiceButton { } void step() { std::string name = audioInterface->getDeviceName(audioInterface->deviceId); - if (name.empty()) - text = "(no device)"; - else - text = name; + text = name.empty() ? "(no device)" : ellipsize(name, 14); } }; @@ -245,8 +317,7 @@ struct SampleRateItem : MenuItem { AudioInterface *audioInterface; float sampleRate; void onAction() { - audioInterface->sampleRate = sampleRate; - audioInterface->openDevice(); + audioInterface->setSampleRate(sampleRate); } }; @@ -262,7 +333,7 @@ struct SampleRateChoice : ChoiceButton { SampleRateItem *item = new SampleRateItem(); item->audioInterface = audioInterface; item->sampleRate = sampleRates[i]; - item->text = stringf("%.0f", sampleRates[i]); + item->text = stringf("%.0f Hz", sampleRates[i]); menu->pushChild(item); } @@ -270,7 +341,7 @@ struct SampleRateChoice : ChoiceButton { gScene->setOverlay(overlay); } void step() { - this->text = stringf("%.0f", audioInterface->sampleRate); + this->text = stringf("%.0f Hz", audioInterface->sampleRate); } }; @@ -279,8 +350,7 @@ struct BlockSizeItem : MenuItem { AudioInterface *audioInterface; int blockSize; void onAction() { - audioInterface->blockSize = blockSize; - audioInterface->openDevice(); + audioInterface->setBlockSize(blockSize); } }; @@ -381,10 +451,6 @@ AudioInterfaceWidget::AudioInterfaceWidget() : ModuleWidget(new AudioInterface() addInput(createInput(Vec(75, yPos), module, AudioInterface::AUDIO2_INPUT)); yPos += 35 + margin; - addInput(createInput(Vec(25, yPos), module, AudioInterface::AUDIO3_INPUT)); - addInput(createInput(Vec(75, yPos), module, AudioInterface::AUDIO4_INPUT)); - yPos += 35 + margin; - { Label *label = new Label(); label->box.pos = Vec(margin, yPos); @@ -397,10 +463,6 @@ AudioInterfaceWidget::AudioInterfaceWidget() : ModuleWidget(new AudioInterface() addOutput(createOutput(Vec(25, yPos), module, AudioInterface::AUDIO1_OUTPUT)); addOutput(createOutput(Vec(75, yPos), module, AudioInterface::AUDIO2_OUTPUT)); yPos += 35 + margin; - - addOutput(createOutput(Vec(25, yPos), module, AudioInterface::AUDIO3_OUTPUT)); - addOutput(createOutput(Vec(75, yPos), module, AudioInterface::AUDIO4_OUTPUT)); - yPos += 35 + margin; } void AudioInterfaceWidget::draw(NVGcontext *vg) { diff --git a/src/dsp/minBLEP.cpp b/src/dsp/minBLEP.cpp new file mode 100644 index 00000000..53fa9b76 --- /dev/null +++ b/src/dsp/minBLEP.cpp @@ -0,0 +1,251 @@ + // MinBLEP Generation Code +// By Daniel Werner +// This Code Is Public Domain + +#include + +#define PI 3.14159265358979f + +// SINC Function +static float SINC(float x) +{ + float pix; + + if(x == 0.0f) + return 1.0f; + else + { + pix = PI * x; + return sinf(pix) / pix; + } +} + +// Generate Blackman Window +static void BlackmanWindow(int n, float *w) +{ + int m = n - 1; + int i; + float f1, f2, fm; + + fm = (float)m; + for(i = 0; i <= m; i++) + { + f1 = (2.0f * PI * (float)i) / fm; + f2 = 2.0f * f1; + w[i] = 0.42f - (0.5f * cosf(f1)) + (0.08f * cosf(f2)); + } +} + +// Discrete Fourier Transform +static void DFT(int n, float *realTime, float *imagTime, float *realFreq, float *imagFreq) +{ + int k, i; + float sr, si, p; + + for(k = 0; k < n; k++) + { + realFreq[k] = 0.0f; + imagFreq[k] = 0.0f; + } + + for(k = 0; k < n; k++) + for(i = 0; i < n; i++) + { + p = (2.0f * PI * (float)(k * i)) / n; + sr = cosf(p); + si = -sinf(p); + realFreq[k] += (realTime[i] * sr) - (imagTime[i] * si); + imagFreq[k] += (realTime[i] * si) + (imagTime[i] * sr); + } +} + +// Inverse Discrete Fourier Transform +static void InverseDFT(int n, float *realTime, float *imagTime, float *realFreq, float *imagFreq) +{ + int k, i; + float sr, si, p; + + for(k = 0; k < n; k++) + { + realTime[k] = 0.0f; + imagTime[k] = 0.0f; + } + + for(k = 0; k < n; k++) + { + for(i = 0; i < n; i++) + { + p = (2.0f * PI * (float)(k * i)) / n; + sr = cosf(p); + si = -sinf(p); + realTime[k] += (realFreq[i] * sr) + (imagFreq[i] * si); + imagTime[k] += (realFreq[i] * si) - (imagFreq[i] * sr); + } + realTime[k] /= n; + imagTime[k] /= n; + } +} + +// Complex Absolute Value +static float cabs(float x, float y) +{ + return sqrtf((x * x) + (y * y)); +} + +// Complex Exponential +static void cexp(float x, float y, float *zx, float *zy) +{ + float expx; + + expx = expf(x); + *zx = expx * cosf(y); + *zy = expx * sinf(y); +} + +// Compute Real Cepstrum Of Signal +static void RealCepstrum(int n, float *signal, float *realCepstrum) +{ + float *realTime, *imagTime, *realFreq, *imagFreq; + int i; + + realTime = new float[n]; + imagTime = new float[n]; + realFreq = new float[n]; + imagFreq = new float[n]; + + // Compose Complex FFT Input + + for(i = 0; i < n; i++) + { + realTime[i] = signal[i]; + imagTime[i] = 0.0f; + } + + // Perform DFT + + DFT(n, realTime, imagTime, realFreq, imagFreq); + + // Calculate Log Of Absolute Value + + for(i = 0; i < n; i++) + { + realFreq[i] = logf(cabs(realFreq[i], imagFreq[i])); + imagFreq[i] = 0.0f; + } + + // Perform Inverse FFT + + InverseDFT(n, realTime, imagTime, realFreq, imagFreq); + + // Output Real Part Of FFT + for(i = 0; i < n; i++) + realCepstrum[i] = realTime[i]; + + delete realTime; + delete imagTime; + delete realFreq; + delete imagFreq; +} + +// Compute Minimum Phase Reconstruction Of Signal +static void MinimumPhase(int n, float *realCepstrum, float *minimumPhase) +{ + int i, nd2; + float *realTime, *imagTime, *realFreq, *imagFreq; + + nd2 = n / 2; + realTime = new float[n]; + imagTime = new float[n]; + realFreq = new float[n]; + imagFreq = new float[n]; + + if((n % 2) == 1) + { + realTime[0] = realCepstrum[0]; + for(i = 1; i < nd2; i++) + realTime[i] = 2.0f * realCepstrum[i]; + for(i = nd2; i < n; i++) + realTime[i] = 0.0f; + } + else + { + realTime[0] = realCepstrum[0]; + for(i = 1; i < nd2; i++) + realTime[i] = 2.0f * realCepstrum[i]; + realTime[nd2] = realCepstrum[nd2]; + for(i = nd2 + 1; i < n; i++) + realTime[i] = 0.0f; + } + + for(i = 0; i < n; i++) + imagTime[i] = 0.0f; + + DFT(n, realTime, imagTime, realFreq, imagFreq); + + for(i = 0; i < n; i++) + cexp(realFreq[i], imagFreq[i], &realFreq[i], &imagFreq[i]); + + InverseDFT(n, realTime, imagTime, realFreq, imagFreq); + + for(i = 0; i < n; i++) + minimumPhase[i] = realTime[i]; + + delete realTime; + delete imagTime; + delete realFreq; + delete imagFreq; +} + +// Generate MinBLEP And Return It In An Array Of Floating Point Values +float *generateMinBLEP(int zeroCrossings, int overSampling) +{ + int i, n; + float r, a, b; + float *buffer1, *buffer2, *minBLEP; + + n = (zeroCrossings * 2 * overSampling) + 1; + + buffer1 = new float[n]; + buffer2 = new float[n]; + + // Generate Sinc + + a = (float)-zeroCrossings; + b = (float)zeroCrossings; + for(i = 0; i < n; i++) + { + r = ((float)i) / ((float)(n - 1)); + buffer1[i] = SINC(a + (r * (b - a))); + } + + // Window Sinc + + BlackmanWindow(n, buffer2); + for(i = 0; i < n; i++) + buffer1[i] *= buffer2[i]; + + // Minimum Phase Reconstruction + + RealCepstrum(n, buffer1, buffer2); + MinimumPhase(n, buffer2, buffer1); + + // Integrate Into MinBLEP + + minBLEP = new float[n]; + a = 0.0f; + for(i = 0; i < n; i++) + { + a += buffer1[i]; + minBLEP[i] = a; + } + + // Normalize + a = minBLEP[n - 1]; + a = 1.0f / a; + for(i = 0; i < n; i++) + minBLEP[i] *= a; + + delete buffer1; + delete buffer2; + return minBLEP; +} \ No newline at end of file diff --git a/src/gui.cpp b/src/gui.cpp index b52e3010..a113c803 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -276,7 +276,7 @@ int loadImage(std::string filename) { auto it = images.find(filename); if (it == images.end()) { // Load image - imageId = nvgCreateImage(vg, filename.c_str(), NVG_IMAGE_REPEATX | NVG_IMAGE_REPEATY); + imageId = nvgCreateImage(vg, filename.c_str(), NVG_IMAGE_REPEATX | NVG_IMAGE_REPEATY | NVG_IMAGE_NEAREST); if (imageId == 0) { printf("Failed to load image %s\n", filename.c_str()); } diff --git a/src/util.cpp b/src/util.cpp index becac658..b7fa8f70 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -1,6 +1,7 @@ #include "util.hpp" #include #include +#include namespace rack { @@ -25,6 +26,7 @@ float randomNormal(){ return normalDist(rng); } + std::string stringf(const char *format, ...) { va_list ap; va_start(ap, format); @@ -32,13 +34,19 @@ std::string stringf(const char *format, ...) { if (size < 0) return ""; size++; - char *buf = new char[size]; - vsnprintf(buf, size, format, ap); + std::string s; + s.resize(size); + vsnprintf(&s[0], size, format, ap); va_end(ap); - std::string s = buf; - delete[] buf; return s; } +std::string ellipsize(std::string s, size_t len) { + if (s.size() <= len) + return s; + else + return s.substr(0, len - 3) + "..."; +} + } // namespace rack diff --git a/src/widgets/ModuleWidget.cpp b/src/widgets/ModuleWidget.cpp index 4d7349be..bb524c20 100644 --- a/src/widgets/ModuleWidget.cpp +++ b/src/widgets/ModuleWidget.cpp @@ -100,10 +100,19 @@ void ModuleWidget::draw(NVGcontext *vg) { bndBevel(vg, box.pos.x, box.pos.y, box.size.x, box.size.y); // CPU usage text - if (module) { - std::string text = stringf("%.1f%%", module->cpuTime * 100.0); + if (true) { + float cpuTime = module ? module->cpuTime : 0.0; + std::string text = stringf("%.1f%%", cpuTime * 100.0); nvgSave(vg); - bndSlider(vg, box.pos.x, box.pos.y, box.size.x, BND_WIDGET_HEIGHT, BND_CORNER_ALL, BND_DEFAULT, module->cpuTime, text.c_str(), NULL); + + nvgBeginPath(vg); + cpuTime = clampf(cpuTime, 0.0, 1.0); + const float barWidth = 15.0; + nvgRect(vg, box.pos.x, box.pos.y + (1.0 - cpuTime) * box.size.y, barWidth, cpuTime * box.size.y); + nvgFillColor(vg, nvgHSLA(0.33 * cubic(1.0 - cpuTime), 1.0, 0.5, 128)); + nvgFill(vg); + + bndLabel(vg, box.pos.x, box.pos.y + box.size.y - BND_WIDGET_HEIGHT, box.size.x, BND_WIDGET_HEIGHT, -1, text.c_str()); nvgRestore(vg); } } @@ -190,4 +199,4 @@ void ModuleWidget::onMouseDown(int button) { } -} // namespace rack +} // namespace rack \ No newline at end of file diff --git a/src/widgets/RackWidget.cpp b/src/widgets/RackWidget.cpp index 0db0b99a..b216d64e 100644 --- a/src/widgets/RackWidget.cpp +++ b/src/widgets/RackWidget.cpp @@ -64,6 +64,14 @@ json_t *RackWidget::toJson() { json_t *versionJ = json_string(gApplicationVersion.c_str()); json_object_set_new(root, "version", versionJ); + // wireOpacity + json_t *wireOpacityJ = json_real(gScene->toolbar->wireOpacitySlider->value); + json_object_set_new(root, "wireOpacity", wireOpacityJ); + + // wireTension + json_t *wireTensionJ = json_real(gScene->toolbar->wireTensionSlider->value); + json_object_set_new(root, "wireTension", wireTensionJ); + // modules json_t *modulesJ = json_array(); std::map moduleIds; @@ -121,6 +129,24 @@ json_t *RackWidget::toJson() { } void RackWidget::fromJson(json_t *root) { + // version + json_t *versionJ = json_object_get(root, "version"); + if (versionJ) { + const char *version = json_string_value(versionJ); + if (gApplicationVersion != version) + printf("JSON version mismatch, attempting to convert JSON version %s to %s\n", version, gApplicationVersion.c_str()); + } + + // wireOpacity + json_t *wireOpacityJ = json_object_get(root, "wireOpacity"); + if (wireOpacityJ) + gScene->toolbar->wireOpacitySlider->value = json_number_value(wireOpacityJ); + + // wireTension + json_t *wireTensionJ = json_object_get(root, "wireTension"); + if (wireTensionJ) + gScene->toolbar->wireTensionSlider->value = json_number_value(wireTensionJ); + // modules std::map moduleWidgets; json_t *modulesJ = json_object_get(root, "modules"); diff --git a/src/widgets/Toolbar.cpp b/src/widgets/Toolbar.cpp index 9f8cdd80..bbf83612 100644 --- a/src/widgets/Toolbar.cpp +++ b/src/widgets/Toolbar.cpp @@ -62,7 +62,7 @@ struct FileChoice : ChoiceButton { struct SampleRateItem : MenuItem { float sampleRate; void onAction() { - printf("\"\"\"\"\"\"\"\"switching\"\"\"\"\"\"\"\" sample rate to %f\n", sampleRate); + gRack->sampleRate = sampleRate; } }; @@ -73,18 +73,10 @@ struct SampleRateChoice : ChoiceButton { menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y)); menu->box.size.x = box.size.x; - { - MenuLabel *item = new MenuLabel(); - item->text = "(sample rate switching not yet implemented)"; - menu->pushChild(item); - } - float sampleRates[6] = {44100, 48000, 88200, 96000, 176400, 192000}; for (int i = 0; i < 6; i++) { SampleRateItem *item = new SampleRateItem(); - char text[100]; - snprintf(text, 100, "%.0f Hz", sampleRates[i]); - item->text = std::string(text); + item->text = stringf("%.0f Hz", sampleRates[i]); item->sampleRate = sampleRates[i]; menu->pushChild(item); } @@ -92,6 +84,9 @@ struct SampleRateChoice : ChoiceButton { overlay->addChild(menu); gScene->setOverlay(overlay); } + void step() { + text = stringf("%.0f Hz", gRack->sampleRate); + } }; diff --git a/src/widgets/WireWidget.cpp b/src/widgets/WireWidget.cpp index 95106eb9..503a65f2 100644 --- a/src/widgets/WireWidget.cpp +++ b/src/widgets/WireWidget.cpp @@ -87,18 +87,21 @@ void WireWidget::updateWire() { void WireWidget::draw(NVGcontext *vg) { Vec outputPos, inputPos; Vec absolutePos = getAbsolutePos(); + float wireOpacity = gScene->toolbar->wireOpacitySlider->value / 100.0; if (outputPort) { outputPos = Rect(outputPort->getAbsolutePos(), outputPort->box.size).getCenter(); } else { outputPos = gMousePos; + wireOpacity = 1.0; } if (inputPort) { inputPos = Rect(inputPort->getAbsolutePos(), inputPort->box.size).getCenter(); } else { inputPos = gMousePos; + wireOpacity = 1.0; } outputPos = outputPos.minus(absolutePos); @@ -107,7 +110,6 @@ void WireWidget::draw(NVGcontext *vg) { bndNodePort(vg, outputPos.x, outputPos.y, BND_DEFAULT, color); bndNodePort(vg, inputPos.x, inputPos.y, BND_DEFAULT, color); nvgSave(vg); - float wireOpacity = gScene->toolbar->wireOpacitySlider->value / 100.0; if (wireOpacity > 0.0) { nvgGlobalAlpha(vg, wireOpacity); float tension = gScene->toolbar->wireTensionSlider->value;