Browse Source

fix Linux audio by using asynchronous Portaudio instead of synchronous

tags/v0.3.0
Andrew Belt 7 years ago
parent
commit
70e3ab97b6
10 changed files with 174 additions and 114 deletions
  1. +6
    -3
      Makefile
  2. +40
    -20
      include/components.hpp
  3. +4
    -4
      include/dsp.hpp
  4. +34
    -0
      include/util.hpp
  5. +76
    -46
      src/core/AudioInterface.cpp
  6. +1
    -32
      src/engine.cpp
  7. +1
    -1
      src/main.cpp
  8. +1
    -1
      src/scene.cpp
  9. +3
    -3
      src/widgets/Toolbar.cpp
  10. +8
    -4
      src/widgets/WireWidget.cpp

+ 6
- 3
Makefile View File

@@ -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

+ 40
- 20
include/components.hpp View File

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


+ 4
- 4
include/dsp.hpp View File

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


+ 34
- 0
include/util.hpp View File

@@ -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

+ 76
- 46
src/core/AudioInterface.cpp View File

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


+ 1
- 32
src/engine.cpp View File

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



+ 1
- 1
src/main.cpp View File

@@ -20,7 +20,7 @@ int main() {
assert(success);
CFRelease(bundleURL);

// chdir(dirname(path));
chdir(dirname(path));
}
#endif



+ 1
- 1
src/scene.cpp View File

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



+ 3
- 3
src/widgets/Toolbar.cpp View File

@@ -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";


+ 8
- 4
src/widgets/WireWidget.cpp View File

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


Loading…
Cancel
Save