| @@ -8,14 +8,17 @@ | |||
| namespace rack { | |||
| /** S must be a power of 2 */ | |||
| /** A simple cyclic buffer. | |||
| S must be a power of 2. | |||
| push() is constant time O(1) | |||
| */ | |||
| template <typename T, size_t S> | |||
| struct RingBuffer { | |||
| T data[S] = {}; | |||
| T data[S]; | |||
| size_t start = 0; | |||
| size_t end = 0; | |||
| size_t mask(size_t i) { | |||
| size_t mask(size_t i) const { | |||
| return i & (S - 1); | |||
| } | |||
| void push(T t) { | |||
| @@ -25,26 +28,29 @@ struct RingBuffer { | |||
| T shift() { | |||
| return data[mask(start++)]; | |||
| } | |||
| bool empty() { | |||
| bool empty() const { | |||
| return start >= end; | |||
| } | |||
| bool full() { | |||
| bool full() const { | |||
| return end - start >= S; | |||
| } | |||
| size_t size() { | |||
| size_t size() const { | |||
| return end - start; | |||
| } | |||
| }; | |||
| /** S must be a power of 2 */ | |||
| /** A cyclic buffer which maintains a valid linear array of size S by keeping a copy of the buffer in adjacent memory. | |||
| S must be a power of 2. | |||
| push() is constant time O(2) relative to RingBuffer | |||
| */ | |||
| template <typename T, size_t S> | |||
| struct DoubleRingBuffer { | |||
| T data[S*2] = {}; | |||
| T data[S*2]; | |||
| size_t start = 0; | |||
| size_t end = 0; | |||
| size_t mask(size_t i) { | |||
| size_t mask(size_t i) const { | |||
| return i & (S - 1); | |||
| } | |||
| void push(T t) { | |||
| @@ -55,21 +61,51 @@ struct DoubleRingBuffer { | |||
| T shift() { | |||
| return data[mask(start++)]; | |||
| } | |||
| bool empty() { | |||
| bool empty() const { | |||
| return start >= end; | |||
| } | |||
| bool full() { | |||
| bool full() const { | |||
| return end - start >= S; | |||
| } | |||
| size_t size() { | |||
| size_t size() const { | |||
| return end - start; | |||
| } | |||
| /** Returns a pointer to S consecutive elements for appending. | |||
| If any data is appended, you must call endIncr afterwards. | |||
| Pointer is invalidated when any other method is called. | |||
| */ | |||
| T *endData() { | |||
| return &data[mask(end)]; | |||
| } | |||
| void endIncr(size_t n) { | |||
| size_t mend = mask(end) + n; | |||
| if (mend > S) { | |||
| // Copy data backward from the doubled block to the main block | |||
| memcpy(data, &data[S], sizeof(T) * (mend - S)); | |||
| // Don't bother copying forward | |||
| } | |||
| end += n; | |||
| } | |||
| /** Returns a pointer to S consecutive elements for consumption | |||
| If any data is consumed, call startIncr afterwards. | |||
| */ | |||
| const T *startData() const { | |||
| return &data[mask(start)]; | |||
| } | |||
| void startIncr(size_t n) { | |||
| start += n; | |||
| } | |||
| }; | |||
| /** A cyclic buffer which maintains a valid linear array of size S by sliding along a larger block of size N. | |||
| The linear array of S elements are moved back to the start of the block once it outgrows past the end. | |||
| This happens every N - S pushes, so the push() time is O(1 + S / (N - S)). | |||
| For example, a float buffer of size 64 in a block of size 1024 is nearly as efficient as RingBuffer. | |||
| */ | |||
| template <typename T, size_t S, size_t N> | |||
| struct AppleRingBuffer { | |||
| T data[N] = {}; | |||
| T data[N]; | |||
| size_t start = 0; | |||
| size_t end = 0; | |||
| @@ -85,19 +121,34 @@ struct AppleRingBuffer { | |||
| T shift() { | |||
| return data[start++]; | |||
| } | |||
| bool empty() { | |||
| bool empty() const { | |||
| return start >= end; | |||
| } | |||
| bool full() { | |||
| bool full() const { | |||
| return end - start >= S; | |||
| } | |||
| size_t size() { | |||
| size_t size() const { | |||
| return end - start; | |||
| } | |||
| /** Returns a pointer to S consecutive elements for appending, requesting to append n elements. | |||
| */ | |||
| T *endData(size_t n) { | |||
| // TODO | |||
| return &data[end]; | |||
| } | |||
| /** Returns a pointer to S consecutive elements for consumption | |||
| If any data is consumed, call startIncr afterwards. | |||
| */ | |||
| const T *startData() const { | |||
| return &data[start]; | |||
| } | |||
| void startIncr(size_t n) { | |||
| // This is valid as long as n < S | |||
| start += n; | |||
| } | |||
| }; | |||
| template <size_t S> | |||
| struct SampleRateConverter { | |||
| SRC_STATE *state; | |||
| SRC_DATA data; | |||
| @@ -116,17 +167,13 @@ struct SampleRateConverter { | |||
| void setRatio(float r) { | |||
| data.src_ratio = r; | |||
| } | |||
| void push(const float *in, int length) { | |||
| float out[S]; | |||
| void process(float *in, int inLength, float *out, int outLength) { | |||
| data.data_in = in; | |||
| data.input_frames = length; | |||
| data.input_frames = inLength; | |||
| data.data_out = out; | |||
| data.output_frames = S; | |||
| data.output_frames = outLength; | |||
| src_process(state, &data); | |||
| } | |||
| void push(float in) { | |||
| push(&in, 1); | |||
| } | |||
| }; | |||
| @@ -4,17 +4,14 @@ | |||
| #include <list> | |||
| #include <vector> | |||
| #include <memory> | |||
| #include <set> | |||
| #include <thread> | |||
| #include <mutex> | |||
| #include "widgets.hpp" | |||
| namespace rack { | |||
| extern std::string gApplicationName; | |||
| extern std::string gApplicationVersion; | |||
| extern Scene *gScene; | |||
| extern RackWidget *gRackWidget; | |||
| //////////////////// | |||
| // Plugin manager | |||
| //////////////////// | |||
| @@ -75,10 +72,6 @@ void drawImage(NVGcontext *vg, Vec pos, int imageId); | |||
| // rack.cpp | |||
| //////////////////// | |||
| // TODO Find a clean way to make this a variable | |||
| #define SAMPLE_RATE 44100 | |||
| struct Wire; | |||
| struct Module { | |||
| @@ -106,17 +99,27 @@ struct Wire { | |||
| float value = 0.0; | |||
| }; | |||
| void rackInit(); | |||
| void rackDestroy(); | |||
| void rackStart(); | |||
| void rackStop(); | |||
| // Does not transfer ownership | |||
| void rackAddModule(Module *module); | |||
| void rackRemoveModule(Module *module); | |||
| // Does not transfer ownership | |||
| void rackConnectWire(Wire *wire); | |||
| void rackDisconnectWire(Wire *wire); | |||
| void rackSetParamSmooth(Module *module, int paramId, float value); | |||
| struct Rack { | |||
| Rack(); | |||
| ~Rack(); | |||
| /** Launches rack thread */ | |||
| void start(); | |||
| void stop(); | |||
| void run(); | |||
| void step(); | |||
| /** Does not transfer pointer ownership */ | |||
| void addModule(Module *module); | |||
| void removeModule(Module *module); | |||
| /** Does not transfer pointer ownership */ | |||
| void addWire(Wire *wire); | |||
| void removeWire(Wire *wire); | |||
| void setParamSmooth(Module *module, int paramId, float value); | |||
| float sampleRate; | |||
| struct Impl; | |||
| Impl *impl; | |||
| }; | |||
| //////////////////// | |||
| // Optional helpers for plugins | |||
| @@ -186,6 +189,17 @@ Screw *createScrew(Vec pos) { | |||
| return screw; | |||
| } | |||
| //////////////////// | |||
| // Globals | |||
| //////////////////// | |||
| extern std::string gApplicationName; | |||
| extern std::string gApplicationVersion; | |||
| extern Scene *gScene; | |||
| extern RackWidget *gRackWidget; | |||
| extern Rack *gRack; | |||
| } // namespace rack | |||
| @@ -3,6 +3,7 @@ | |||
| #include <stdint.h> | |||
| #include <math.h> | |||
| #include <random> | |||
| #include <string> | |||
| namespace rack { | |||
| @@ -183,5 +184,12 @@ struct Rect { | |||
| } | |||
| }; | |||
| //////////////////// | |||
| // Helper functions | |||
| //////////////////// | |||
| /** Converts a printf format string and optional arguments into a std::string */ | |||
| std::string stringf(const char *format, ...); | |||
| } // namespace rack | |||
| @@ -0,0 +1,216 @@ | |||
| #include <stdio.h> | |||
| #include <stdlib.h> | |||
| #include <assert.h> | |||
| #include <math.h> | |||
| #include <chrono> | |||
| #include <condition_variable> | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| /** Threads which obtain a VIPLock will cause wait() to block for other less important threads. | |||
| This does not provide the VIPs with an exclusive lock. That should be left up to another mutex shared between the less important thread. | |||
| */ | |||
| struct VIPMutex { | |||
| int count = 0; | |||
| std::condition_variable cv; | |||
| std::mutex countMutex; | |||
| /** Blocks until there are no remaining VIPLocks */ | |||
| void wait() { | |||
| std::unique_lock<std::mutex> lock(countMutex); | |||
| while (count > 0) | |||
| cv.wait(lock); | |||
| } | |||
| }; | |||
| struct VIPLock { | |||
| VIPMutex &m; | |||
| VIPLock(VIPMutex &m) : m(m) { | |||
| std::unique_lock<std::mutex> lock(m.countMutex); | |||
| m.count++; | |||
| } | |||
| ~VIPLock() { | |||
| std::unique_lock<std::mutex> lock(m.countMutex); | |||
| m.count--; | |||
| lock.unlock(); | |||
| m.cv.notify_all(); | |||
| } | |||
| }; | |||
| struct Rack::Impl { | |||
| bool running = false; | |||
| std::mutex mutex; | |||
| std::thread audioThread; | |||
| VIPMutex vipMutex; | |||
| std::set<Module*> modules; | |||
| // Merely used for keeping track of which module inputs point to which module outputs, to prevent pointer mistakes and make the rack API more rigorous | |||
| std::set<Wire*> wires; | |||
| // Parameter interpolation | |||
| Module *smoothModule = NULL; | |||
| int smoothParamId; | |||
| float smoothValue; | |||
| }; | |||
| Rack::Rack() { | |||
| impl = new Rack::Impl(); | |||
| sampleRate = 44100.0; | |||
| } | |||
| Rack::~Rack() { | |||
| // Make sure there are no wires or modules in the rack on destruction. This suggests that a module failed to remove itself before the GUI was destroyed. | |||
| assert(impl->wires.empty()); | |||
| assert(impl->modules.empty()); | |||
| delete impl; | |||
| } | |||
| void Rack::start() { | |||
| impl->running = true; | |||
| impl->audioThread = std::thread(&Rack::run, this); | |||
| } | |||
| void Rack::stop() { | |||
| impl->running = false; | |||
| impl->audioThread.join(); | |||
| } | |||
| void Rack::run() { | |||
| // Every time the rack waits and locks a mutex, it steps this many frames | |||
| const int stepSize = 32; | |||
| while (impl->running) { | |||
| impl->vipMutex.wait(); | |||
| auto start = std::chrono::high_resolution_clock::now(); | |||
| { | |||
| std::lock_guard<std::mutex> lock(impl->mutex); | |||
| for (int i = 0; i < stepSize; i++) { | |||
| step(); | |||
| } | |||
| } | |||
| auto end = std::chrono::high_resolution_clock::now(); | |||
| auto duration = std::chrono::nanoseconds((long) (0.9 * 1e9 * stepSize / sampleRate)) - (end - start); | |||
| // Avoid pegging the CPU at 100% when there are no "blocking" modules like AudioInterface, but still step audio at a reasonable rate | |||
| std::this_thread::sleep_for(duration); | |||
| } | |||
| } | |||
| void Rack::step() { | |||
| // Param interpolation | |||
| if (impl->smoothModule) { | |||
| float value = impl->smoothModule->params[impl->smoothParamId]; | |||
| const float lambda = 60.0; // decay rate is 1 graphics frame | |||
| const float snap = 0.0001; | |||
| float delta = impl->smoothValue - value; | |||
| if (fabsf(delta) < snap) { | |||
| impl->smoothModule->params[impl->smoothParamId] = impl->smoothValue; | |||
| impl->smoothModule = NULL; | |||
| } | |||
| else { | |||
| value += delta * lambda / sampleRate; | |||
| impl->smoothModule->params[impl->smoothParamId] = value; | |||
| } | |||
| } | |||
| // Step all modules | |||
| std::chrono::time_point<std::chrono::high_resolution_clock> start, end; | |||
| for (Module *module : impl->modules) { | |||
| // Start clock for CPU usage | |||
| start = std::chrono::high_resolution_clock::now(); | |||
| // Step module by one frame | |||
| module->step(); | |||
| // Stop clock and smooth step time value | |||
| end = std::chrono::high_resolution_clock::now(); | |||
| std::chrono::duration<float> diff = end - start; | |||
| float elapsed = diff.count() * sampleRate; | |||
| const float lambda = 1.0; | |||
| module->cpuTime += (elapsed - module->cpuTime) * lambda / sampleRate; | |||
| } | |||
| } | |||
| void Rack::addModule(Module *module) { | |||
| assert(module); | |||
| VIPLock vipLock(impl->vipMutex); | |||
| std::lock_guard<std::mutex> lock(impl->mutex); | |||
| // Check that the module is not already added | |||
| assert(impl->modules.find(module) == impl->modules.end()); | |||
| impl->modules.insert(module); | |||
| } | |||
| void Rack::removeModule(Module *module) { | |||
| assert(module); | |||
| VIPLock vipLock(impl->vipMutex); | |||
| std::lock_guard<std::mutex> lock(impl->mutex); | |||
| // Remove parameter interpolation which point to this module | |||
| if (module == impl->smoothModule) { | |||
| impl->smoothModule = NULL; | |||
| } | |||
| // Check that all wires are disconnected | |||
| for (Wire *wire : impl->wires) { | |||
| assert(wire->outputModule != module); | |||
| assert(wire->inputModule != module); | |||
| } | |||
| auto it = impl->modules.find(module); | |||
| if (it != impl->modules.end()) { | |||
| impl->modules.erase(it); | |||
| } | |||
| } | |||
| void Rack::addWire(Wire *wire) { | |||
| assert(wire); | |||
| VIPLock vipLock(impl->vipMutex); | |||
| std::lock_guard<std::mutex> 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); | |||
| assert(wire->inputModule); | |||
| // Check that the inputs/outputs are not already used by another cable | |||
| for (Wire *wire2 : impl->wires) { | |||
| assert(wire2 != wire); | |||
| assert(!(wire2->outputModule == wire->outputModule && wire2->outputId == wire->outputId)); | |||
| assert(!(wire2->inputModule == wire->inputModule && wire2->inputId == wire->inputId)); | |||
| } | |||
| // Connect the wire to inputModule | |||
| impl->wires.insert(wire); | |||
| wire->inputModule->inputs[wire->inputId] = &wire->value; | |||
| wire->outputModule->outputs[wire->outputId] = &wire->value; | |||
| } | |||
| void Rack::removeWire(Wire *wire) { | |||
| assert(wire); | |||
| VIPLock vipLock(impl->vipMutex); | |||
| std::lock_guard<std::mutex> lock(impl->mutex); | |||
| // Disconnect wire from inputModule | |||
| wire->inputModule->inputs[wire->inputId] = NULL; | |||
| wire->outputModule->outputs[wire->outputId] = NULL; | |||
| auto it = impl->wires.find(wire); | |||
| assert(it != impl->wires.end()); | |||
| impl->wires.erase(it); | |||
| } | |||
| void Rack::setParamSmooth(Module *module, int paramId, float value) { | |||
| VIPLock vipLock(impl->vipMutex); | |||
| std::lock_guard<std::mutex> lock(impl->mutex); | |||
| // Check existing parameter interpolation | |||
| if (impl->smoothModule) { | |||
| if (!(impl->smoothModule == module && impl->smoothParamId == paramId)) { | |||
| // Jump param value to smooth value | |||
| impl->smoothModule->params[impl->smoothParamId] = impl->smoothValue; | |||
| } | |||
| } | |||
| impl->smoothModule = module; | |||
| impl->smoothParamId = paramId; | |||
| impl->smoothValue = value; | |||
| } | |||
| } // namespace rack | |||
| @@ -37,21 +37,25 @@ struct AudioInterface : Module { | |||
| PaStream *stream = NULL; | |||
| int numOutputs; | |||
| int numInputs; | |||
| int numFrames; | |||
| int bufferFrame; | |||
| float outputBuffer[1<<14] = {}; | |||
| float inputBuffer[1<<14] = {}; | |||
| int bufferFrame; | |||
| // Used because the GUI thread and Rack thread can both interact with this class | |||
| std::mutex mutex; | |||
| // Set these and call openDevice() | |||
| int deviceId = -1; | |||
| float sampleRate = 44100.0; | |||
| int blockSize = 256; | |||
| AudioInterface(); | |||
| ~AudioInterface(); | |||
| void step(); | |||
| int getDeviceCount(); | |||
| std::string getDeviceName(int deviceId); | |||
| // Use -1 as the deviceId to close the current device | |||
| void openDevice(int deviceId); | |||
| void openDevice(); | |||
| void closeDevice(); | |||
| }; | |||
| @@ -59,10 +63,11 @@ AudioInterface::AudioInterface() { | |||
| params.resize(NUM_PARAMS); | |||
| inputs.resize(NUM_INPUTS); | |||
| outputs.resize(NUM_OUTPUTS); | |||
| closeDevice(); | |||
| } | |||
| AudioInterface::~AudioInterface() { | |||
| openDevice(-1); | |||
| closeDevice(); | |||
| } | |||
| void AudioInterface::step() { | |||
| @@ -85,14 +90,14 @@ void AudioInterface::step() { | |||
| setf(outputs[i], inputBuffer[numOutputs * bufferFrame + i] * 5.0); | |||
| } | |||
| if (++bufferFrame >= numFrames) { | |||
| if (++bufferFrame >= blockSize) { | |||
| bufferFrame = 0; | |||
| PaError err; | |||
| // Input | |||
| // (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, numFrames); | |||
| err = Pa_ReadStream(stream, inputBuffer, blockSize); | |||
| if (err) { | |||
| // Ignore buffer underflows | |||
| if (err != paInputOverflowed) { | |||
| @@ -103,7 +108,7 @@ void AudioInterface::step() { | |||
| // Output | |||
| if (numOutputs > 0) { | |||
| err = Pa_WriteStream(stream, outputBuffer, numFrames); | |||
| err = Pa_WriteStream(stream, outputBuffer, blockSize); | |||
| if (err) { | |||
| // Ignore buffer underflows | |||
| if (err != paOutputUnderflowed) { | |||
| @@ -123,29 +128,13 @@ std::string AudioInterface::getDeviceName(int deviceId) { | |||
| if (!info) | |||
| return ""; | |||
| const PaHostApiInfo *apiInfo = Pa_GetHostApiInfo(info->hostApi); | |||
| char name[1024]; | |||
| snprintf(name, sizeof(name), "%s: %s (%d in, %d out)", apiInfo->name, info->name, info->maxInputChannels, info->maxOutputChannels); | |||
| return name; | |||
| return stringf("%s: %s (%d in, %d out)", apiInfo->name, info->name, info->maxInputChannels, info->maxOutputChannels); | |||
| } | |||
| void AudioInterface::openDevice(int deviceId) { | |||
| void AudioInterface::openDevice() { | |||
| closeDevice(); | |||
| std::lock_guard<std::mutex> lock(mutex); | |||
| // Close existing device | |||
| if (stream) { | |||
| PaError err; | |||
| err = Pa_CloseStream(stream); | |||
| if (err) { | |||
| // This shouldn't happen: | |||
| printf("Failed to close audio stream: %s\n", Pa_GetErrorText(err)); | |||
| } | |||
| stream = NULL; | |||
| } | |||
| numOutputs = 0; | |||
| numInputs = 0; | |||
| numFrames = 256; | |||
| bufferFrame = 0; | |||
| // Open new device | |||
| if (deviceId >= 0) { | |||
| PaError err; | |||
| @@ -173,10 +162,10 @@ void AudioInterface::openDevice(int deviceId) { | |||
| inputParameters.hostApiSpecificStreamInfo = NULL; | |||
| // Don't use stream parameters if 0 input or output channels | |||
| PaStreamParameters *outputP = numOutputs == 0 ? NULL : &outputParameters; | |||
| PaStreamParameters *inputP = numInputs == 0 ? NULL : &inputParameters; | |||
| err = Pa_OpenStream(&stream, inputP, outputP, SAMPLE_RATE, numFrames, paNoFlag, NULL, NULL); | |||
| err = Pa_OpenStream(&stream, | |||
| numInputs == 0 ? NULL : &outputParameters, | |||
| numOutputs == 0 ? NULL : &inputParameters, | |||
| sampleRate, blockSize, paNoFlag, NULL, NULL); | |||
| if (err) { | |||
| printf("Failed to open audio stream: %s\n", Pa_GetErrorText(err)); | |||
| return; | |||
| @@ -190,12 +179,30 @@ void AudioInterface::openDevice(int deviceId) { | |||
| } | |||
| } | |||
| void AudioInterface::closeDevice() { | |||
| std::lock_guard<std::mutex> lock(mutex); | |||
| if (stream) { | |||
| PaError err; | |||
| err = Pa_CloseStream(stream); | |||
| if (err) { | |||
| // This shouldn't happen: | |||
| printf("Failed to close audio stream: %s\n", Pa_GetErrorText(err)); | |||
| } | |||
| stream = NULL; | |||
| } | |||
| numOutputs = 0; | |||
| numInputs = 0; | |||
| bufferFrame = 0; | |||
| } | |||
| struct AudioItem : MenuItem { | |||
| AudioInterface *audioInterface; | |||
| int deviceId; | |||
| void onAction() { | |||
| audioInterface->openDevice(deviceId); | |||
| audioInterface->deviceId = deviceId; | |||
| audioInterface->openDevice(); | |||
| } | |||
| }; | |||
| @@ -224,6 +231,81 @@ struct AudioChoice : ChoiceButton { | |||
| overlay->addChild(menu); | |||
| gScene->setOverlay(overlay); | |||
| } | |||
| void step() { | |||
| std::string name = audioInterface->getDeviceName(audioInterface->deviceId); | |||
| if (name.empty()) | |||
| text = "(no device)"; | |||
| else | |||
| text = name; | |||
| } | |||
| }; | |||
| struct SampleRateItem : MenuItem { | |||
| AudioInterface *audioInterface; | |||
| float sampleRate; | |||
| void onAction() { | |||
| audioInterface->sampleRate = sampleRate; | |||
| audioInterface->openDevice(); | |||
| } | |||
| }; | |||
| struct SampleRateChoice : ChoiceButton { | |||
| AudioInterface *audioInterface; | |||
| void onAction() { | |||
| MenuOverlay *overlay = new MenuOverlay(); | |||
| Menu *menu = new Menu(); | |||
| menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y)); | |||
| const float sampleRates[6] = {44100, 48000, 88200, 96000, 176400, 192000}; | |||
| for (int i = 0; i < 6; i++) { | |||
| SampleRateItem *item = new SampleRateItem(); | |||
| item->audioInterface = audioInterface; | |||
| item->sampleRate = sampleRates[i]; | |||
| item->text = stringf("%.0f", sampleRates[i]); | |||
| menu->pushChild(item); | |||
| } | |||
| overlay->addChild(menu); | |||
| gScene->setOverlay(overlay); | |||
| } | |||
| void step() { | |||
| this->text = stringf("%.0f", audioInterface->sampleRate); | |||
| } | |||
| }; | |||
| struct BlockSizeItem : MenuItem { | |||
| AudioInterface *audioInterface; | |||
| int blockSize; | |||
| void onAction() { | |||
| audioInterface->blockSize = blockSize; | |||
| audioInterface->openDevice(); | |||
| } | |||
| }; | |||
| struct BlockSizeChoice : ChoiceButton { | |||
| AudioInterface *audioInterface; | |||
| void onAction() { | |||
| MenuOverlay *overlay = new MenuOverlay(); | |||
| Menu *menu = new Menu(); | |||
| menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y)); | |||
| const int blockSizes[6] = {128, 256, 512, 1024, 2048, 4096}; | |||
| for (int i = 0; i < 6; i++) { | |||
| BlockSizeItem *item = new BlockSizeItem(); | |||
| item->audioInterface = audioInterface; | |||
| item->blockSize = blockSizes[i]; | |||
| item->text = stringf("%d", blockSizes[i]); | |||
| menu->pushChild(item); | |||
| } | |||
| overlay->addChild(menu); | |||
| gScene->setOverlay(overlay); | |||
| } | |||
| void step() { | |||
| this->text = stringf("%d", audioInterface->blockSize); | |||
| } | |||
| }; | |||
| @@ -242,13 +324,48 @@ AudioInterfaceWidget::AudioInterfaceWidget() : ModuleWidget(new AudioInterface() | |||
| } | |||
| { | |||
| AudioChoice *audioChoice = new AudioChoice(); | |||
| audioChoice->audioInterface = dynamic_cast<AudioInterface*>(module); | |||
| audioChoice->text = "Audio device"; | |||
| audioChoice->box.pos = Vec(margin, yPos); | |||
| audioChoice->box.size.x = box.size.x - 10; | |||
| addChild(audioChoice); | |||
| yPos += audioChoice->box.size.y + 2*margin; | |||
| Label *label = new Label(); | |||
| label->box.pos = Vec(margin, yPos); | |||
| label->text = "Audio device"; | |||
| addChild(label); | |||
| yPos += label->box.size.y + margin; | |||
| AudioChoice *choice = new AudioChoice(); | |||
| choice->audioInterface = dynamic_cast<AudioInterface*>(module); | |||
| choice->box.pos = Vec(margin, yPos); | |||
| choice->box.size.x = box.size.x - 10; | |||
| addChild(choice); | |||
| yPos += choice->box.size.y + 2*margin; | |||
| } | |||
| { | |||
| Label *label = new Label(); | |||
| label->box.pos = Vec(margin, yPos); | |||
| label->text = "Sample rate"; | |||
| addChild(label); | |||
| yPos += label->box.size.y + margin; | |||
| SampleRateChoice *choice = new SampleRateChoice(); | |||
| choice->audioInterface = dynamic_cast<AudioInterface*>(module); | |||
| choice->box.pos = Vec(margin, yPos); | |||
| choice->box.size.x = box.size.x - 10; | |||
| addChild(choice); | |||
| yPos += choice->box.size.y + 2*margin; | |||
| } | |||
| { | |||
| Label *label = new Label(); | |||
| label->box.pos = Vec(margin, yPos); | |||
| label->text = "Block size"; | |||
| addChild(label); | |||
| yPos += label->box.size.y + margin; | |||
| BlockSizeChoice *choice = new BlockSizeChoice(); | |||
| choice->audioInterface = dynamic_cast<AudioInterface*>(module); | |||
| choice->box.pos = Vec(margin, yPos); | |||
| choice->box.size.x = box.size.x - 10; | |||
| addChild(choice); | |||
| yPos += choice->box.size.y + 2*margin; | |||
| } | |||
| { | |||
| @@ -92,9 +92,7 @@ std::string MidiInterface::getPortName(int portId) { | |||
| const PmDeviceInfo *info = Pm_GetDeviceInfo(portId); | |||
| if (!info) | |||
| return ""; | |||
| char name[1024]; | |||
| snprintf(name, sizeof(name), "%s: %s (%s)", info->interf, info->name, info->input ? "input" : "output"); | |||
| return name; | |||
| return stringf("%s: %s (%s)", info->interf, info->name, info->input ? "input" : "output"); | |||
| } | |||
| void MidiInterface::openPort(int portId) { | |||
| @@ -1,7 +1,9 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| rack::Plugin *coreInit(); | |||
| using namespace rack; | |||
| Plugin *coreInit(); | |||
| void audioInit(); | |||
| void midiInit(); | |||
| @@ -10,12 +12,12 @@ void midiInit(); | |||
| // module widgets | |||
| //////////////////// | |||
| struct AudioInterfaceWidget : rack::ModuleWidget { | |||
| struct AudioInterfaceWidget : ModuleWidget { | |||
| AudioInterfaceWidget(); | |||
| void draw(NVGcontext *vg); | |||
| }; | |||
| struct MidiInterfaceWidget : rack::ModuleWidget { | |||
| struct MidiInterfaceWidget : ModuleWidget { | |||
| MidiInterfaceWidget(); | |||
| void draw(NVGcontext *vg); | |||
| }; | |||
| @@ -1,5 +1,5 @@ | |||
| #include <unistd.h> | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| #include <GL/glew.h> | |||
| #include <GLFW/glfw3.h> | |||
| @@ -4,7 +4,7 @@ | |||
| #include <libgen.h> | |||
| #endif | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -12,14 +12,16 @@ namespace rack { | |||
| std::string gApplicationName = "Virtuoso Rack"; | |||
| std::string gApplicationVersion = "v0.0.0 alpha"; | |||
| Rack *gRack; | |||
| } // namespace rack | |||
| using namespace rack; | |||
| int main() { | |||
| // Set working directory | |||
| #if defined(APPLE) | |||
| // macOS workaround for setting the working directory to the location of the .app | |||
| { | |||
| CFBundleRef bundle = CFBundleGetMainBundle(); | |||
| CFURLRef bundleURL = CFBundleCopyBundleURL(bundle); | |||
| @@ -32,19 +34,18 @@ int main() { | |||
| } | |||
| #endif | |||
| rackInit(); | |||
| gRack = new Rack(); | |||
| guiInit(); | |||
| pluginInit(); | |||
| gRackWidget->loadPatch("autosave.json"); | |||
| rackStart(); | |||
| gRack->start(); | |||
| guiRun(); | |||
| rackStop(); | |||
| gRack->stop(); | |||
| gRackWidget->savePatch("autosave.json"); | |||
| guiDestroy(); | |||
| rackDestroy(); | |||
| delete gRack; | |||
| pluginDestroy(); | |||
| return 0; | |||
| } | |||
| @@ -7,7 +7,7 @@ | |||
| #include <glob.h> | |||
| #endif | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| #include "core/core.hpp" | |||
| @@ -1,202 +0,0 @@ | |||
| #include <stdio.h> | |||
| #include <stdlib.h> | |||
| #include <assert.h> | |||
| #include <math.h> | |||
| #include <set> | |||
| #include <chrono> | |||
| #include <thread> | |||
| #include <mutex> | |||
| #include <condition_variable> | |||
| #include "Rack.hpp" | |||
| namespace rack { | |||
| static std::thread thread; | |||
| static std::mutex mutex; | |||
| static bool running; | |||
| static std::set<Module*> modules; | |||
| // Merely used for keeping track of which module inputs point to which module outputs, to prevent pointer mistakes and make the rack API rigorous | |||
| static std::set<Wire*> wires; | |||
| // Parameter interpolation | |||
| static Module *smoothModule = NULL; | |||
| static int smoothParamId; | |||
| static float smoothValue; | |||
| // HACK | |||
| static std::mutex requestMutex; | |||
| static std::condition_variable requestCv; | |||
| static bool request = false; | |||
| struct RackRequest { | |||
| RackRequest() { | |||
| std::unique_lock<std::mutex> lock(requestMutex); | |||
| request = true; | |||
| } | |||
| ~RackRequest() { | |||
| std::unique_lock<std::mutex> lock(requestMutex); | |||
| request = false; | |||
| lock.unlock(); | |||
| requestCv.notify_all(); | |||
| } | |||
| }; | |||
| // END HACK | |||
| void rackInit() { | |||
| } | |||
| void rackDestroy() { | |||
| // Make sure there are no wires or modules in the rack on destruction. This suggests that a module failed to remove itself before the GUI was destroyed. | |||
| assert(wires.empty()); | |||
| assert(modules.empty()); | |||
| } | |||
| void rackStep() { | |||
| // Param interpolation | |||
| if (smoothModule) { | |||
| float value = smoothModule->params[smoothParamId]; | |||
| const float lambda = 60.0; // decay rate is 1 graphics frame | |||
| const float snap = 0.0001; | |||
| float delta = smoothValue - value; | |||
| if (fabsf(delta) < snap) { | |||
| smoothModule->params[smoothParamId] = smoothValue; | |||
| smoothModule = NULL; | |||
| } | |||
| else { | |||
| value += delta * lambda / SAMPLE_RATE; | |||
| smoothModule->params[smoothParamId] = value; | |||
| } | |||
| } | |||
| // Step all modules | |||
| std::chrono::time_point<std::chrono::high_resolution_clock> start, end; | |||
| for (Module *module : modules) { | |||
| // Start clock for CPU usage | |||
| start = std::chrono::high_resolution_clock::now(); | |||
| // Step module by one frame | |||
| module->step(); | |||
| // Stop clock and smooth step time value | |||
| end = std::chrono::high_resolution_clock::now(); | |||
| std::chrono::duration<float> diff = end - start; | |||
| float elapsed = diff.count() * SAMPLE_RATE; | |||
| const float lambda = 1.0; | |||
| module->cpuTime += (elapsed - module->cpuTime) * lambda / SAMPLE_RATE; | |||
| } | |||
| } | |||
| static void rackRun() { | |||
| // Every time the rack waits and locks a mutex, it steps this many frames | |||
| const int stepSize = 32; | |||
| while (running) { | |||
| auto start = std::chrono::high_resolution_clock::now(); | |||
| // This lock is to make sure the GUI gets higher priority than this thread | |||
| { | |||
| std::unique_lock<std::mutex> lock(requestMutex); | |||
| while (request) | |||
| requestCv.wait(lock); | |||
| } | |||
| { | |||
| std::lock_guard<std::mutex> lock(mutex); | |||
| for (int i = 0; i < stepSize; i++) { | |||
| rackStep(); | |||
| } | |||
| } | |||
| auto end = std::chrono::high_resolution_clock::now(); | |||
| auto duration = std::chrono::nanoseconds((long) (0.9 * 1e9 * stepSize / SAMPLE_RATE)) - (end - start); | |||
| std::this_thread::sleep_for(duration); | |||
| } | |||
| } | |||
| void rackStart() { | |||
| running = true; | |||
| thread = std::thread(rackRun); | |||
| } | |||
| void rackStop() { | |||
| running = false; | |||
| thread.join(); | |||
| } | |||
| void rackAddModule(Module *module) { | |||
| assert(module); | |||
| RackRequest rm; | |||
| std::lock_guard<std::mutex> lock(mutex); | |||
| // Check that the module is not already added | |||
| assert(modules.find(module) == modules.end()); | |||
| modules.insert(module); | |||
| } | |||
| void rackRemoveModule(Module *module) { | |||
| assert(module); | |||
| RackRequest rm; | |||
| std::lock_guard<std::mutex> lock(mutex); | |||
| // Remove parameter interpolation which point to this module | |||
| if (module == smoothModule) { | |||
| smoothModule = NULL; | |||
| } | |||
| // Check that all wires are disconnected | |||
| for (Wire *wire : wires) { | |||
| assert(wire->outputModule != module); | |||
| assert(wire->inputModule != module); | |||
| } | |||
| auto it = modules.find(module); | |||
| if (it != modules.end()) { | |||
| modules.erase(it); | |||
| } | |||
| } | |||
| void rackConnectWire(Wire *wire) { | |||
| assert(wire); | |||
| RackRequest rm; | |||
| std::lock_guard<std::mutex> lock(mutex); | |||
| // It would probably be good to reset the wire voltage | |||
| wire->value = 0.0; | |||
| // Check that the wire is not already added | |||
| assert(wires.find(wire) == wires.end()); | |||
| assert(wire->outputModule); | |||
| assert(wire->inputModule); | |||
| // Check that the inputs/outputs are not already used by another cable | |||
| for (Wire *wire2 : wires) { | |||
| assert(wire2 != wire); | |||
| assert(!(wire2->outputModule == wire->outputModule && wire2->outputId == wire->outputId)); | |||
| assert(!(wire2->inputModule == wire->inputModule && wire2->inputId == wire->inputId)); | |||
| } | |||
| // Connect the wire to inputModule | |||
| wires.insert(wire); | |||
| wire->inputModule->inputs[wire->inputId] = &wire->value; | |||
| wire->outputModule->outputs[wire->outputId] = &wire->value; | |||
| } | |||
| void rackDisconnectWire(Wire *wire) { | |||
| assert(wire); | |||
| RackRequest rm; | |||
| std::lock_guard<std::mutex> lock(mutex); | |||
| // Disconnect wire from inputModule | |||
| wire->inputModule->inputs[wire->inputId] = NULL; | |||
| wire->outputModule->outputs[wire->outputId] = NULL; | |||
| auto it = wires.find(wire); | |||
| assert(it != wires.end()); | |||
| wires.erase(it); | |||
| } | |||
| void rackSetParamSmooth(Module *module, int paramId, float value) { | |||
| // Check existing parameter interpolation | |||
| if (smoothModule) { | |||
| if (!(smoothModule == module && smoothParamId == paramId)) { | |||
| // Jump param value to smooth value | |||
| smoothModule->params[smoothParamId] = smoothValue; | |||
| } | |||
| } | |||
| smoothModule = module; | |||
| smoothParamId = paramId; | |||
| smoothValue = value; | |||
| } | |||
| } // namespace rack | |||
| @@ -1,4 +1,6 @@ | |||
| #include "util.hpp" | |||
| #include <stdio.h> | |||
| #include <stdarg.h> | |||
| namespace rack { | |||
| @@ -23,5 +25,20 @@ float randomNormal(){ | |||
| return normalDist(rng); | |||
| } | |||
| std::string stringf(const char *format, ...) { | |||
| va_list ap; | |||
| va_start(ap, format); | |||
| size_t size = vsnprintf(NULL, 0, format, ap); | |||
| if (size < 0) | |||
| return ""; | |||
| size++; | |||
| char *buf = new char[size]; | |||
| vsnprintf(buf, size, format, ap); | |||
| va_end(ap); | |||
| std::string s = buf; | |||
| delete[] buf; | |||
| return s; | |||
| } | |||
| } // namespace rack | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -6,7 +6,7 @@ namespace rack { | |||
| ModuleWidget::ModuleWidget(Module *module) { | |||
| this->module = module; | |||
| if (module) { | |||
| rackAddModule(module); | |||
| gRack->addModule(module); | |||
| } | |||
| } | |||
| @@ -14,7 +14,7 @@ ModuleWidget::~ModuleWidget() { | |||
| // Make sure WireWidget destructors are called *before* removing `module` from the rack. | |||
| disconnectPorts(); | |||
| if (module) { | |||
| rackRemoveModule(module); | |||
| gRack->removeModule(module); | |||
| delete module; | |||
| } | |||
| } | |||
| @@ -101,10 +101,9 @@ void ModuleWidget::draw(NVGcontext *vg) { | |||
| // CPU usage text | |||
| if (module) { | |||
| char text[32]; | |||
| snprintf(text, sizeof(text), "%.2f%%", module->cpuTime * 10); | |||
| std::string text = stringf("%.1f%%", module->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, NULL); | |||
| 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); | |||
| nvgRestore(vg); | |||
| } | |||
| } | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -23,7 +23,7 @@ void ParamWidget::onChange() { | |||
| return; | |||
| // moduleWidget->module->params[paramId] = value; | |||
| rackSetParamSmooth(module, paramId, value); | |||
| gRack->setParamSmooth(module, paramId, value); | |||
| } | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| #include <algorithm> | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| #include <algorithm> | |||
| @@ -1,4 +1,4 @@ | |||
| #include "Rack.hpp" | |||
| #include "rack.hpp" | |||
| namespace rack { | |||
| @@ -70,7 +70,7 @@ WireWidget::~WireWidget() { | |||
| void WireWidget::updateWire() { | |||
| if (wire) { | |||
| rackDisconnectWire(wire); | |||
| gRack->removeWire(wire); | |||
| delete wire; | |||
| wire = NULL; | |||
| } | |||
| @@ -80,7 +80,7 @@ void WireWidget::updateWire() { | |||
| wire->outputId = outputPort->outputId; | |||
| wire->inputModule = inputPort->module; | |||
| wire->inputId = inputPort->inputId; | |||
| rackConnectWire(wire); | |||
| gRack->addWire(wire); | |||
| } | |||
| } | |||