@@ -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); | |||
} | |||
} | |||