@@ -49,13 +49,16 @@ LDFLAGS += \ | |||
-L$(HOME)/pkg/portaudio-r1891-build/lib/x64/ReleaseMinDependency -lportaudio_x64 \ | |||
-Wl,--export-all-symbols,--out-implib,libRack.a -mwindows | |||
TARGET = Rack.exe | |||
OBJECTS = Rack.res | |||
all: $(TARGET) | |||
# OBJECTS = Rack.res | |||
%.res: %.rc | |||
windres $^ -O coff -o $@ | |||
endif | |||
all: $(TARGET) | |||
clean: | |||
rm -rf $(TARGET) build | |||
include Makefile.inc |
@@ -10,30 +10,37 @@ namespace rack { | |||
struct KnobDavies1900h : SpriteKnob { | |||
KnobDavies1900h() { | |||
box.size = Vec(36, 36); | |||
spriteOffset = Vec(-8, -8); | |||
spriteSize = Vec(64, 64); | |||
minIndex = 44; | |||
maxIndex = -46; | |||
// box.size = Vec(36, 36); | |||
// spriteOffset = Vec(-8, -8); | |||
// spriteSize = Vec(64, 64); | |||
// minIndex = 44; | |||
// maxIndex = -46; | |||
// spriteCount = 120; | |||
box.size = Vec(42, 42); | |||
spriteOffset = Vec(-9, -9); | |||
spriteSize = Vec(60, 60); | |||
minIndex = 0; | |||
maxIndex = 119; | |||
spriteCount = 120; | |||
spriteImage = Image::load("res/Black Plastic small 01.png"); | |||
} | |||
}; | |||
struct KnobDavies1900hWhite : KnobDavies1900h { | |||
KnobDavies1900hWhite() { | |||
spriteImage = Image::load("res/ComponentLibrary/Davies1900hWhite.png"); | |||
// spriteImage = Image::load("res/ComponentLibrary/Davies1900hWhite.png"); | |||
} | |||
}; | |||
struct KnobDavies1900hBlack : KnobDavies1900h { | |||
KnobDavies1900hBlack() { | |||
spriteImage = Image::load("res/ComponentLibrary/Davies1900hBlack.png"); | |||
// spriteImage = Image::load("res/ComponentLibrary/Davies1900hBlack.png"); | |||
} | |||
}; | |||
struct KnobDavies1900hRed : KnobDavies1900h { | |||
KnobDavies1900hRed() { | |||
spriteImage = Image::load("res/ComponentLibrary/Davies1900hRed.png"); | |||
// spriteImage = Image::load("res/ComponentLibrary/Davies1900hRed.png"); | |||
} | |||
}; | |||
@@ -43,10 +50,14 @@ struct KnobDavies1900hRed : KnobDavies1900h { | |||
struct PJ301M : SpriteWidget { | |||
PJ301M() { | |||
box.size = Vec(24, 24); | |||
spriteOffset = Vec(-10, -10); | |||
spriteSize = Vec(48, 48); | |||
spriteImage = Image::load("res/ComponentLibrary/PJ301M.png"); | |||
// box.size = Vec(24, 24); | |||
// spriteOffset = Vec(-10, -10); | |||
// spriteSize = Vec(48, 48); | |||
// spriteImage = Image::load("res/ComponentLibrary/PJ301M.png"); | |||
box.size = Vec(26, 26); | |||
spriteOffset = Vec(-16, -16); | |||
spriteSize = Vec(56, 56); | |||
spriteImage = Image::load("res/port.png"); | |||
} | |||
}; | |||
struct InputPortPJ301M : InputPort, PJ301M {}; | |||
@@ -54,10 +65,14 @@ struct OutputPortPJ301M: OutputPort, PJ301M {}; | |||
struct PJ3410 : SpriteWidget { | |||
PJ3410() { | |||
box.size = Vec(31, 31); | |||
spriteOffset = Vec(-9, -9); | |||
spriteSize = Vec(54, 54); | |||
spriteImage = Image::load("res/ComponentLibrary/PJ3410.png"); | |||
// box.size = Vec(31, 31); | |||
// spriteOffset = Vec(-9, -9); | |||
// spriteSize = Vec(54, 54); | |||
// spriteImage = Image::load("res/ComponentLibrary/PJ3410.png"); | |||
box.size = Vec(26, 26); | |||
spriteOffset = Vec(-12, -12); | |||
spriteSize = Vec(56, 56); | |||
spriteImage = Image::load("res/port.png"); | |||
} | |||
}; | |||
struct InputPortPJ3410 : InputPort, PJ3410 {}; | |||
@@ -65,10 +80,15 @@ struct OutputPortPJ3410: OutputPort, PJ3410 {}; | |||
struct CL1362 : SpriteWidget { | |||
CL1362() { | |||
box.size = Vec(33, 29); | |||
spriteOffset = Vec(-10, -10); | |||
spriteSize = Vec(57, 54); | |||
spriteImage = Image::load("res/ComponentLibrary/CL1362.png"); | |||
// box.size = Vec(33, 29); | |||
// spriteOffset = Vec(-10, -10); | |||
// spriteSize = Vec(57, 54); | |||
// spriteImage = Image::load("res/ComponentLibrary/CL1362.png"); | |||
box.size = Vec(26, 26); | |||
spriteOffset = Vec(-12, -12); | |||
spriteSize = Vec(56, 56); | |||
spriteImage = Image::load("res/port.png"); | |||
} | |||
}; | |||
struct InputPortCL1362 : InputPort, CL1362 {}; | |||
@@ -251,11 +251,11 @@ struct SampleRateConverter { | |||
data.src_ratio = r; | |||
} | |||
/** `in` and `out` are interlaced with the number of channels */ | |||
void process(const float *in, int *inFrames, float *out, int *outFrames) { | |||
// The const cast is okay since src_process does not modify it | |||
data.data_in = const_cast<float*>(in); | |||
void process(const Frame<CHANNELS> *in, int *inFrames, Frame<CHANNELS> *out, int *outFrames) { | |||
// Old versions of libsamplerate use float* here instead of const float* | |||
data.data_in = (float*) in; | |||
data.input_frames = *inFrames; | |||
data.data_out = out; | |||
data.data_out = (float*) out; | |||
data.output_frames = *outFrames; | |||
src_process(state, &data); | |||
*inFrames = data.input_frames_used; | |||
@@ -8,6 +8,8 @@ | |||
#include <assert.h> | |||
#include <string> | |||
#include <condition_variable> | |||
#include <mutex> | |||
namespace rack { | |||
@@ -33,4 +35,36 @@ 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); | |||
/** 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(); | |||
} | |||
}; | |||
} // namespace rack |
@@ -1,5 +1,6 @@ | |||
#include <assert.h> | |||
#include <mutex> | |||
#include <thread> | |||
#include <portaudio.h> | |||
#include "core.hpp" | |||
#include "dsp.hpp" | |||
@@ -40,7 +41,8 @@ struct AudioInterface : Module { | |||
int numInputs = 0; | |||
// Used because the GUI thread and Rack thread can both interact with this class | |||
std::mutex mutex; | |||
std::mutex bufferMutex; | |||
bool streamRunning; | |||
SampleRateConverter<2> inputSrc; | |||
SampleRateConverter<2> outputSrc; | |||
@@ -54,6 +56,7 @@ struct AudioInterface : Module { | |||
AudioInterface(); | |||
~AudioInterface(); | |||
void step(); | |||
void stepStream(const float *input, float *output, int numFrames); | |||
int getDeviceCount(); | |||
std::string getDeviceName(int deviceId); | |||
@@ -84,10 +87,23 @@ AudioInterface::~AudioInterface() { | |||
} | |||
void AudioInterface::step() { | |||
std::lock_guard<std::mutex> lock(mutex); | |||
if (!stream) | |||
return; | |||
// Read/write stream if we have enough input, OR the output buffer is empty if we have no input | |||
if (numOutputs > 0) { | |||
while (inputSrcBuffer.size() >= blockSize && streamRunning) { | |||
std::this_thread::sleep_for(std::chrono::duration<float>(100e-6)); | |||
} | |||
} | |||
else if (numInputs > 0) { | |||
while (outputBuffer.empty() && streamRunning) { | |||
std::this_thread::sleep_for(std::chrono::duration<float>(100e-6)); | |||
} | |||
} | |||
std::lock_guard<std::mutex> lock(bufferMutex); | |||
// Get input and pass it through the sample rate converter | |||
if (numOutputs > 0) { | |||
if (!inputBuffer.full()) { | |||
@@ -98,63 +114,63 @@ void AudioInterface::step() { | |||
} | |||
// Once full, sample rate convert the input | |||
// inputBuffer -> SRC -> inputSrcBuffer | |||
if (inputBuffer.full()) { | |||
inputSrc.setRatio(sampleRate / gSampleRate); | |||
int inLen = inputBuffer.size(); | |||
int outLen = inputSrcBuffer.capacity(); | |||
inputSrc.process((const float*) inputBuffer.startData(), &inLen, (float*) inputSrcBuffer.endData(), &outLen); | |||
inputSrc.process(inputBuffer.startData(), &inLen, inputSrcBuffer.endData(), &outLen); | |||
inputBuffer.startIncr(inLen); | |||
inputSrcBuffer.endIncr(outLen); | |||
} | |||
} | |||
// Read/write stream if we have enough input, OR the output buffer is empty if we have no input | |||
bool streamReady = (numOutputs > 0) ? (inputSrcBuffer.size() >= blockSize) : (outputBuffer.empty()); | |||
if (streamReady) { | |||
// printf("%p\t%d\t%d\n", this, inputSrcBuffer.size(), outputBuffer.size()); | |||
PaError err; | |||
// 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) { | |||
Frame<2> *buf = new Frame<2>[blockSize]; | |||
err = Pa_ReadStream(stream, (float*) buf, blockSize); | |||
if (err) { | |||
// Ignore buffer underflows | |||
if (err == paInputOverflowed) { | |||
fprintf(stderr, "Audio input buffer underflow\n"); | |||
} | |||
} | |||
// Set output | |||
if (!outputBuffer.empty()) { | |||
Frame<2> f = outputBuffer.shift(); | |||
setf(outputs[AUDIO1_OUTPUT], 5.0 * f.samples[0]); | |||
setf(outputs[AUDIO2_OUTPUT], 5.0 * f.samples[1]); | |||
} | |||
} | |||
// Pass output through sample rate converter | |||
outputSrc.setRatio(gSampleRate / sampleRate); | |||
int inLen = blockSize; | |||
int outLen = outputBuffer.capacity(); | |||
outputSrc.process((float*) buf, &inLen, (float*) outputBuffer.endData(), &outLen); | |||
outputBuffer.endIncr(outLen); | |||
delete[] buf; | |||
void AudioInterface::stepStream(const float *input, float *output, int numFrames) { | |||
if (numOutputs > 0) { | |||
// Wait for enough input before proceeding | |||
while (inputSrcBuffer.size() < numFrames) { | |||
if (!streamRunning) | |||
return; | |||
std::this_thread::sleep_for(std::chrono::duration<float>(100e-6)); | |||
} | |||
} | |||
// Write input to output stream | |||
if (numOutputs > 0) { | |||
assert(inputSrcBuffer.size() >= blockSize); | |||
err = Pa_WriteStream(stream, (const float*) inputSrcBuffer.startData(), blockSize); | |||
inputSrcBuffer.startIncr(blockSize); | |||
if (err) { | |||
// Ignore buffer underflows | |||
if (err == paOutputUnderflowed) { | |||
fprintf(stderr, "Audio output buffer underflow\n"); | |||
} | |||
std::lock_guard<std::mutex> lock(bufferMutex); | |||
// input stream -> output buffer | |||
if (numInputs > 0) { | |||
Frame<2> inputFrames[numFrames]; | |||
for (int i = 0; i < numFrames; i++) { | |||
for (int c = 0; c < 2; c++) { | |||
inputFrames[i].samples[c] = (numInputs > c) ? input[i*numInputs + c] : 0.0; | |||
} | |||
} | |||
// Pass output through sample rate converter | |||
outputSrc.setRatio(gSampleRate / sampleRate); | |||
int inLen = numFrames; | |||
int outLen = outputBuffer.capacity(); | |||
outputSrc.process(inputFrames, &inLen, outputBuffer.endData(), &outLen); | |||
outputBuffer.endIncr(outLen); | |||
} | |||
// Set output | |||
if (!outputBuffer.empty()) { | |||
Frame<2> f = outputBuffer.shift(); | |||
setf(outputs[AUDIO1_OUTPUT], 5.0 * f.samples[0]); | |||
setf(outputs[AUDIO2_OUTPUT], 5.0 * f.samples[1]); | |||
// input buffer -> output stream | |||
if (numOutputs > 0) { | |||
for (int i = 0; i < numFrames; i++) { | |||
if (inputSrcBuffer.empty()) | |||
break; | |||
Frame<2> f = inputSrcBuffer.shift(); | |||
for (int c = 0; c < numOutputs; c++) { | |||
output[i*numOutputs + c] = f.samples[c]; | |||
} | |||
} | |||
} | |||
} | |||
@@ -170,9 +186,15 @@ std::string AudioInterface::getDeviceName(int deviceId) { | |||
return stringf("%s: %s (%d in, %d out)", apiInfo->name, info->name, info->maxInputChannels, info->maxOutputChannels); | |||
} | |||
static int paCallback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData) { | |||
AudioInterface *p = (AudioInterface *) userData; | |||
p->stepStream((const float *) inputBuffer, (float *) outputBuffer, framesPerBuffer); | |||
return paContinue; | |||
} | |||
void AudioInterface::openDevice(int deviceId, float sampleRate, int blockSize) { | |||
closeDevice(); | |||
std::lock_guard<std::mutex> lock(mutex); | |||
std::lock_guard<std::mutex> lock(bufferMutex); | |||
this->sampleRate = sampleRate; | |||
this->blockSize = blockSize; | |||
@@ -207,7 +229,7 @@ void AudioInterface::openDevice(int deviceId, float sampleRate, int blockSize) { | |||
err = Pa_OpenStream(&stream, | |||
numInputs == 0 ? NULL : &inputParameters, | |||
numOutputs == 0 ? NULL : &outputParameters, | |||
sampleRate, blockSize, paNoFlag, NULL, NULL); | |||
sampleRate, blockSize, paNoFlag, paCallback, this); | |||
if (err) { | |||
fprintf(stderr, "Failed to open audio stream: %s\n", Pa_GetErrorText(err)); | |||
return; | |||
@@ -218,6 +240,8 @@ void AudioInterface::openDevice(int deviceId, float sampleRate, int blockSize) { | |||
fprintf(stderr, "Failed to start audio stream: %s\n", Pa_GetErrorText(err)); | |||
return; | |||
} | |||
// This should go after Pa_StartStream because sometimes it will call the callback once synchronously, and that time it should return early | |||
streamRunning = true; | |||
// Correct sample rate | |||
const PaStreamInfo *streamInfo = Pa_GetStreamInfo(stream); | |||
@@ -227,10 +251,16 @@ void AudioInterface::openDevice(int deviceId, float sampleRate, int blockSize) { | |||
} | |||
void AudioInterface::closeDevice() { | |||
std::lock_guard<std::mutex> lock(mutex); | |||
std::lock_guard<std::mutex> lock(bufferMutex); | |||
if (stream) { | |||
PaError err; | |||
streamRunning = false; | |||
err = Pa_StopStream(stream); | |||
if (err) { | |||
fprintf(stderr, "Failed to stop audio stream: %s\n", Pa_GetErrorText(err)); | |||
} | |||
err = Pa_CloseStream(stream); | |||
if (err) { | |||
fprintf(stderr, "Failed to close audio stream: %s\n", Pa_GetErrorText(err)); | |||
@@ -5,47 +5,16 @@ | |||
#include <set> | |||
#include <chrono> | |||
#include <thread> | |||
#include <mutex> | |||
#include <condition_variable> | |||
#include <xmmintrin.h> | |||
#include "engine.hpp" | |||
#include "util.hpp" | |||
namespace rack { | |||
float gSampleRate; | |||
/** 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(); | |||
} | |||
}; | |||
static bool running = false; | |||
@@ -20,7 +20,7 @@ int main() { | |||
assert(success); | |||
CFRelease(bundleURL); | |||
// chdir(dirname(path)); | |||
chdir(dirname(path)); | |||
} | |||
#endif | |||
@@ -4,7 +4,7 @@ | |||
namespace rack { | |||
std::string gApplicationName = "VCV Rack"; | |||
std::string gApplicationVersion = "v0.2.0 alpha"; | |||
std::string gApplicationVersion = "v0.1.1 alpha"; | |||
RackWidget *gRackWidget = NULL; | |||
@@ -47,9 +47,9 @@ struct FileChoice : ChoiceButton { | |||
openItem->text = "Open"; | |||
menu->pushChild(openItem); | |||
MenuItem *saveItem = new SaveItem(); | |||
saveItem->text = "Save"; | |||
menu->pushChild(saveItem); | |||
// MenuItem *saveItem = new SaveItem(); | |||
// saveItem->text = "Save"; | |||
// menu->pushChild(saveItem); | |||
MenuItem *saveAsItem = new SaveItem(); | |||
saveAsItem->text = "Save As"; | |||
@@ -76,13 +76,17 @@ static NVGcolor wireColors[8] = { | |||
nvgRGB(0x88, 0x88, 0x88), // light gray | |||
nvgRGB(0xaa, 0xaa, 0xaa), // white | |||
}; | |||
static int nextWireColorId = 1; | |||
static int lastWireColorId = -1; | |||
WireWidget::WireWidget() { | |||
color = wireColors[nextWireColorId]; | |||
nextWireColorId = (nextWireColorId + 1) % 8; | |||
int wireColorId; | |||
do { | |||
wireColorId = randomu32() % 8; | |||
} while (wireColorId == lastWireColorId); | |||
lastWireColorId = wireColorId; | |||
color = wireColors[wireColorId]; | |||
} | |||
WireWidget::~WireWidget() { | |||