| @@ -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() { | |||