to AudioInterfacetags/v0.3.0
@@ -29,6 +29,9 @@ CFLAGS += -DNOC_FILE_DIALOG_OSX | |||||
CXXFLAGS += -DAPPLE -stdlib=libc++ -I$(HOME)/local/include | CXXFLAGS += -DAPPLE -stdlib=libc++ -I$(HOME)/local/include | ||||
LDFLAGS += -stdlib=libc++ -L$(HOME)/local/lib -lpthread -lglew -lglfw3 -framework Cocoa -framework OpenGL -framework IOKit -framework CoreVideo -ldl -ljansson -lportaudio -lportmidi | LDFLAGS += -stdlib=libc++ -L$(HOME)/local/lib -lpthread -lglew -lglfw3 -framework Cocoa -framework OpenGL -framework IOKit -framework CoreVideo -ldl -ljansson -lportaudio -lportmidi | ||||
TARGET = Rack | TARGET = Rack | ||||
Rack.app: $(TARGET) | |||||
./bundle.sh | |||||
endif | endif | ||||
# Windows | # Windows | ||||
@@ -1,8 +1,6 @@ | |||||
*Note: This software is in semi-public alpha. If you have stumbled upon this project, feel free to try it out and report bugs to the GitHub issue tracker. However, with more users it becomes difficult to make breaking changes, so please don't spread this to your friends and the Internet just yet, until the official announcement has been made.* | *Note: This software is in semi-public alpha. If you have stumbled upon this project, feel free to try it out and report bugs to the GitHub issue tracker. However, with more users it becomes difficult to make breaking changes, so please don't spread this to your friends and the Internet just yet, until the official announcement has been made.* | ||||
░█▀▄░█▀█░█▀▀░█░█ | |||||
░█▀▄░█▀█░█░░░█▀▄ | |||||
░▀░▀░▀░▀░▀▀▀░▀░▀ | |||||
# Rack | |||||
Eurorack-style modular DAW | Eurorack-style modular DAW | ||||
@@ -162,6 +162,9 @@ struct QuantityWidget : virtual Widget { | |||||
struct Label : Widget { | struct Label : Widget { | ||||
std::string text; | std::string text; | ||||
Label() { | |||||
box.size.y = BND_WIDGET_HEIGHT; | |||||
} | |||||
void draw(NVGcontext *vg); | void draw(NVGcontext *vg); | ||||
}; | }; | ||||
@@ -209,7 +212,9 @@ struct Button : OpaqueWidget { | |||||
std::string text; | std::string text; | ||||
BNDwidgetState state = BND_DEFAULT; | BNDwidgetState state = BND_DEFAULT; | ||||
Button(); | |||||
Button() { | |||||
box.size.y = BND_WIDGET_HEIGHT; | |||||
} | |||||
void draw(NVGcontext *vg); | void draw(NVGcontext *vg); | ||||
void onMouseEnter(); | void onMouseEnter(); | ||||
void onMouseLeave() ; | void onMouseLeave() ; | ||||
@@ -223,7 +228,9 @@ struct ChoiceButton : Button { | |||||
struct Slider : OpaqueWidget, QuantityWidget { | struct Slider : OpaqueWidget, QuantityWidget { | ||||
BNDwidgetState state = BND_DEFAULT; | BNDwidgetState state = BND_DEFAULT; | ||||
Slider(); | |||||
Slider() { | |||||
box.size.y = BND_WIDGET_HEIGHT; | |||||
} | |||||
void draw(NVGcontext *vg); | void draw(NVGcontext *vg); | ||||
void onDragStart(); | void onDragStart(); | ||||
void onDragMove(Vec mouseRel); | void onDragMove(Vec mouseRel); | ||||
@@ -236,7 +243,9 @@ struct ScrollBar : OpaqueWidget { | |||||
float containerSize = 0.0; | float containerSize = 0.0; | ||||
BNDwidgetState state = BND_DEFAULT; | BNDwidgetState state = BND_DEFAULT; | ||||
ScrollBar(); | |||||
ScrollBar() { | |||||
box.size = Vec(BND_SCROLLBAR_WIDTH, BND_SCROLLBAR_HEIGHT); | |||||
} | |||||
void draw(NVGcontext *vg); | void draw(NVGcontext *vg); | ||||
void move(float delta); | void move(float delta); | ||||
void onDragStart(); | void onDragStart(); | ||||
@@ -3,10 +3,16 @@ | |||||
#include <portaudio.h> | #include <portaudio.h> | ||||
#include "core.hpp" | #include "core.hpp" | ||||
using namespace rack; | using namespace rack; | ||||
static bool audioInitialized = false; | |||||
void audioInit() { | |||||
PaError err = Pa_Initialize(); | |||||
if (err) { | |||||
printf("Failed to initialize PortAudio: %s\n", Pa_GetErrorText(err)); | |||||
return; | |||||
} | |||||
} | |||||
struct AudioInterface : Module { | struct AudioInterface : Module { | ||||
@@ -16,15 +22,24 @@ struct AudioInterface : Module { | |||||
enum InputIds { | enum InputIds { | ||||
AUDIO1_INPUT, | AUDIO1_INPUT, | ||||
AUDIO2_INPUT, | AUDIO2_INPUT, | ||||
AUDIO3_INPUT, | |||||
AUDIO4_INPUT, | |||||
NUM_INPUTS | NUM_INPUTS | ||||
}; | }; | ||||
enum OutputIds { | enum OutputIds { | ||||
AUDIO1_OUTPUT, | |||||
AUDIO2_OUTPUT, | |||||
AUDIO3_OUTPUT, | |||||
AUDIO4_OUTPUT, | |||||
NUM_OUTPUTS | NUM_OUTPUTS | ||||
}; | }; | ||||
PaStream *stream = NULL; | PaStream *stream = NULL; | ||||
float *buffer; | |||||
int bufferFrames; | |||||
int numOutputs; | |||||
int numInputs; | |||||
int numFrames; | |||||
float outputBuffer[1<<14] = {}; | |||||
float inputBuffer[1<<14] = {}; | |||||
int bufferFrame; | int bufferFrame; | ||||
// Used because the GUI thread and Rack thread can both interact with this class | // Used because the GUI thread and Rack thread can both interact with this class | ||||
std::mutex mutex; | std::mutex mutex; | ||||
@@ -44,42 +59,52 @@ AudioInterface::AudioInterface() { | |||||
params.resize(NUM_PARAMS); | params.resize(NUM_PARAMS); | ||||
inputs.resize(NUM_INPUTS); | inputs.resize(NUM_INPUTS); | ||||
outputs.resize(NUM_OUTPUTS); | outputs.resize(NUM_OUTPUTS); | ||||
buffer = new float[1<<14]; | |||||
// Lazy initialize PulseAudio | |||||
if (!audioInitialized) { | |||||
PaError err = Pa_Initialize(); | |||||
if (err) { | |||||
printf("Failed to initialize PortAudio: %s\n", Pa_GetErrorText(err)); | |||||
return; | |||||
} | |||||
audioInitialized = true; | |||||
} | |||||
} | } | ||||
AudioInterface::~AudioInterface() { | AudioInterface::~AudioInterface() { | ||||
openDevice(-1); | openDevice(-1); | ||||
delete[] buffer; | |||||
} | } | ||||
void AudioInterface::step() { | void AudioInterface::step() { | ||||
std::lock_guard<std::mutex> lock(mutex); | std::lock_guard<std::mutex> lock(mutex); | ||||
if (!stream) | |||||
if (!stream) { | |||||
setf(inputs[AUDIO1_OUTPUT], 0.0); | |||||
setf(inputs[AUDIO2_OUTPUT], 0.0); | |||||
setf(inputs[AUDIO3_OUTPUT], 0.0); | |||||
setf(inputs[AUDIO4_OUTPUT], 0.0); | |||||
return; | return; | ||||
} | |||||
buffer[2*bufferFrame + 0] = getf(inputs[AUDIO1_INPUT]) / 5.0; | |||||
buffer[2*bufferFrame + 1] = getf(inputs[AUDIO2_INPUT]) / 5.0; | |||||
// Input ports -> Output buffer | |||||
for (int i = 0; i < numOutputs; i++) { | |||||
outputBuffer[numOutputs * bufferFrame + i] = getf(inputs[i]) / 5.0; | |||||
} | |||||
// Input buffer -> Output ports | |||||
for (int i = 0; i < numInputs; i++) { | |||||
setf(outputs[i], inputBuffer[numOutputs * bufferFrame + i] * 5.0); | |||||
} | |||||
if (++bufferFrame >= bufferFrames) { | |||||
if (++bufferFrame >= numFrames) { | |||||
bufferFrame = 0; | bufferFrame = 0; | ||||
PaError err = Pa_WriteStream(stream, buffer, bufferFrames); | |||||
PaError err; | |||||
// Input | |||||
// (for some reason, if you write the output stream before you read the input stream, PortAudio can segfault on Windows.) | |||||
err = Pa_ReadStream(stream, inputBuffer, numFrames); | |||||
if (err) { | |||||
// Ignore buffer underflows | |||||
if (err != paInputOverflowed) { | |||||
printf("Audio input buffer underflow\n"); | |||||
} | |||||
} | |||||
// Output | |||||
err = Pa_WriteStream(stream, outputBuffer, numFrames); | |||||
if (err) { | if (err) { | ||||
// Ignore buffer underflows | // Ignore buffer underflows | ||||
if (err != paOutputUnderflowed) { | if (err != paOutputUnderflowed) { | ||||
printf("Failed to write buffer to audio stream: %s\n", Pa_GetErrorText(err)); | |||||
return; | |||||
printf("Audio output buffer underflow\n"); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -91,40 +116,59 @@ int AudioInterface::getDeviceCount() { | |||||
std::string AudioInterface::getDeviceName(int deviceId) { | std::string AudioInterface::getDeviceName(int deviceId) { | ||||
const PaDeviceInfo *info = Pa_GetDeviceInfo(deviceId); | const PaDeviceInfo *info = Pa_GetDeviceInfo(deviceId); | ||||
return info ? std::string(info->name) : ""; | |||||
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; | |||||
} | } | ||||
void AudioInterface::openDevice(int deviceId) { | void AudioInterface::openDevice(int deviceId) { | ||||
PaError err; | |||||
std::lock_guard<std::mutex> lock(mutex); | std::lock_guard<std::mutex> lock(mutex); | ||||
// Close existing device | // Close existing device | ||||
if (stream) { | if (stream) { | ||||
PaError err; | |||||
err = Pa_CloseStream(stream); | err = Pa_CloseStream(stream); | ||||
if (err) { | if (err) { | ||||
// This shouldn't happen: | |||||
printf("Failed to close audio stream: %s\n", Pa_GetErrorText(err)); | printf("Failed to close audio stream: %s\n", Pa_GetErrorText(err)); | ||||
} | } | ||||
stream = NULL; | stream = NULL; | ||||
} | } | ||||
numOutputs = 0; | |||||
numInputs = 0; | |||||
// Open new device | |||||
bufferFrames = 256; | |||||
numFrames = 256; | |||||
bufferFrame = 0; | bufferFrame = 0; | ||||
// Open new device | |||||
if (deviceId >= 0) { | if (deviceId >= 0) { | ||||
PaError err; | |||||
const PaDeviceInfo *info = Pa_GetDeviceInfo(deviceId); | const PaDeviceInfo *info = Pa_GetDeviceInfo(deviceId); | ||||
if (!info) { | if (!info) { | ||||
printf("Failed to query audio device\n"); | printf("Failed to query audio device\n"); | ||||
return; | return; | ||||
} | } | ||||
numOutputs = mini(info->maxOutputChannels, 4); | |||||
numInputs = mini(info->maxInputChannels, 4); | |||||
PaStreamParameters outputParameters; | PaStreamParameters outputParameters; | ||||
outputParameters.device = deviceId; | outputParameters.device = deviceId; | ||||
outputParameters.channelCount = 2; | |||||
outputParameters.channelCount = numOutputs; | |||||
outputParameters.sampleFormat = paFloat32; | outputParameters.sampleFormat = paFloat32; | ||||
outputParameters.suggestedLatency = info->defaultLowOutputLatency; | outputParameters.suggestedLatency = info->defaultLowOutputLatency; | ||||
outputParameters.hostApiSpecificStreamInfo = NULL; | outputParameters.hostApiSpecificStreamInfo = NULL; | ||||
err = Pa_OpenStream(&stream, NULL, &outputParameters, SAMPLE_RATE, bufferFrames, paNoFlag, NULL, NULL); | |||||
PaStreamParameters inputParameters; | |||||
inputParameters.device = deviceId; | |||||
inputParameters.channelCount = numInputs; | |||||
inputParameters.sampleFormat = paFloat32; | |||||
inputParameters.suggestedLatency = info->defaultLowInputLatency; | |||||
inputParameters.hostApiSpecificStreamInfo = NULL; | |||||
err = Pa_OpenStream(&stream, &inputParameters, &outputParameters, SAMPLE_RATE, numFrames, paNoFlag, NULL, NULL); | |||||
if (err) { | if (err) { | ||||
printf("Failed to open audio stream: %s\n", Pa_GetErrorText(err)); | printf("Failed to open audio stream: %s\n", Pa_GetErrorText(err)); | ||||
return; | return; | ||||
@@ -177,17 +221,60 @@ struct AudioChoice : ChoiceButton { | |||||
AudioInterfaceWidget::AudioInterfaceWidget() : ModuleWidget(new AudioInterface()) { | AudioInterfaceWidget::AudioInterfaceWidget() : ModuleWidget(new AudioInterface()) { | ||||
box.size = Vec(15*8, 380); | box.size = Vec(15*8, 380); | ||||
addInput(createInput(Vec(15, 100), module, AudioInterface::AUDIO1_INPUT)); | |||||
addInput(createInput(Vec(70, 100), module, AudioInterface::AUDIO2_INPUT)); | |||||
float margin = 5; | |||||
float yPos = margin; | |||||
{ | |||||
Label *label = new Label(); | |||||
label->box.pos = Vec(margin, yPos); | |||||
label->text = "Audio Interface"; | |||||
addChild(label); | |||||
yPos += label->box.size.y + margin; | |||||
} | |||||
{ | { | ||||
AudioChoice *audioChoice = new AudioChoice(); | AudioChoice *audioChoice = new AudioChoice(); | ||||
audioChoice->audioInterface = dynamic_cast<AudioInterface*>(module); | audioChoice->audioInterface = dynamic_cast<AudioInterface*>(module); | ||||
audioChoice->text = "Audio Interface"; | audioChoice->text = "Audio Interface"; | ||||
audioChoice->box.pos = Vec(0, 0); | |||||
audioChoice->box.size.x = box.size.x; | |||||
audioChoice->box.pos = Vec(margin, yPos); | |||||
audioChoice->box.size.x = box.size.x - 10; | |||||
addChild(audioChoice); | addChild(audioChoice); | ||||
yPos += audioChoice->box.size.y + 2*margin; | |||||
} | |||||
{ | |||||
Label *label = new Label(); | |||||
label->box.pos = Vec(margin, yPos); | |||||
label->text = "Outputs"; | |||||
addChild(label); | |||||
yPos += label->box.size.y + margin; | |||||
} | } | ||||
yPos += 5; | |||||
addInput(createInput(Vec(25, yPos), module, AudioInterface::AUDIO1_INPUT)); | |||||
addInput(createInput(Vec(75, yPos), module, AudioInterface::AUDIO2_INPUT)); | |||||
yPos += 35 + margin; | |||||
addInput(createInput(Vec(25, yPos), module, AudioInterface::AUDIO3_INPUT)); | |||||
addInput(createInput(Vec(75, yPos), module, AudioInterface::AUDIO4_INPUT)); | |||||
yPos += 35 + margin; | |||||
{ | |||||
Label *label = new Label(); | |||||
label->box.pos = Vec(margin, yPos); | |||||
label->text = "Inputs"; | |||||
addChild(label); | |||||
yPos += label->box.size.y + margin; | |||||
} | |||||
yPos += 5; | |||||
addOutput(createOutput(Vec(25, yPos), module, AudioInterface::AUDIO1_OUTPUT)); | |||||
addOutput(createOutput(Vec(75, yPos), module, AudioInterface::AUDIO2_OUTPUT)); | |||||
yPos += 35 + margin; | |||||
addOutput(createOutput(Vec(25, yPos), module, AudioInterface::AUDIO3_OUTPUT)); | |||||
addOutput(createOutput(Vec(75, yPos), module, AudioInterface::AUDIO4_OUTPUT)); | |||||
yPos += 35 + margin; | |||||
} | } | ||||
void AudioInterfaceWidget::draw(NVGcontext *vg) { | void AudioInterfaceWidget::draw(NVGcontext *vg) { | ||||
@@ -7,7 +7,14 @@ | |||||
using namespace rack; | using namespace rack; | ||||
static bool midiInitialized = false; | |||||
void midiInit() { | |||||
PmError err = Pm_Initialize(); | |||||
if (err) { | |||||
printf("Failed to initialize PortMidi: %s\n", Pm_GetErrorText(err)); | |||||
return; | |||||
} | |||||
} | |||||
struct MidiInterface : Module { | struct MidiInterface : Module { | ||||
@@ -49,16 +56,6 @@ MidiInterface::MidiInterface() { | |||||
params.resize(NUM_PARAMS); | params.resize(NUM_PARAMS); | ||||
inputs.resize(NUM_INPUTS); | inputs.resize(NUM_INPUTS); | ||||
outputs.resize(NUM_OUTPUTS); | outputs.resize(NUM_OUTPUTS); | ||||
// Lazy initialize PortMidi | |||||
if (!midiInitialized) { | |||||
PmError err = Pm_Initialize(); | |||||
if (err) { | |||||
printf("Failed to initialize PortMidi: %s\n", Pm_GetErrorText(err)); | |||||
return; | |||||
} | |||||
midiInitialized = true; | |||||
} | |||||
} | } | ||||
MidiInterface::~MidiInterface() { | MidiInterface::~MidiInterface() { | ||||
@@ -93,7 +90,11 @@ int MidiInterface::getPortCount() { | |||||
std::string MidiInterface::getPortName(int portId) { | std::string MidiInterface::getPortName(int portId) { | ||||
const PmDeviceInfo *info = Pm_GetDeviceInfo(portId); | const PmDeviceInfo *info = Pm_GetDeviceInfo(portId); | ||||
return info ? std::string(info->name) : ""; | |||||
if (!info) | |||||
return ""; | |||||
char name[1024]; | |||||
snprintf(name, sizeof(name), "%s: %s (%s)", info->interf, info->name, info->input ? "input" : "output"); | |||||
return name; | |||||
} | } | ||||
void MidiInterface::openPort(int portId) { | void MidiInterface::openPort(int portId) { | ||||
@@ -223,16 +224,44 @@ struct MidiChoice : ChoiceButton { | |||||
MidiInterfaceWidget::MidiInterfaceWidget() : ModuleWidget(new MidiInterface()) { | MidiInterfaceWidget::MidiInterfaceWidget() : ModuleWidget(new MidiInterface()) { | ||||
box.size = Vec(15*8, 380); | box.size = Vec(15*8, 380); | ||||
addOutput(createOutput(Vec(15, 100), module, MidiInterface::GATE_OUTPUT)); | |||||
addOutput(createOutput(Vec(70, 100), module, MidiInterface::PITCH_OUTPUT)); | |||||
float margin = 5; | |||||
float yPos = margin; | |||||
{ | |||||
Label *label = new Label(); | |||||
label->box.pos = Vec(margin, yPos); | |||||
label->text = "MIDI Interface"; | |||||
addChild(label); | |||||
yPos += label->box.size.y + margin; | |||||
} | |||||
{ | { | ||||
MidiChoice *midiChoice = new MidiChoice(); | MidiChoice *midiChoice = new MidiChoice(); | ||||
midiChoice->midiInterface = dynamic_cast<MidiInterface*>(module); | midiChoice->midiInterface = dynamic_cast<MidiInterface*>(module); | ||||
midiChoice->text = "MIDI Interface"; | midiChoice->text = "MIDI Interface"; | ||||
midiChoice->box.pos = Vec(0, 0); | |||||
midiChoice->box.size.x = box.size.x; | |||||
midiChoice->box.pos = Vec(margin, yPos); | |||||
midiChoice->box.size.x = box.size.x - 10; | |||||
addChild(midiChoice); | addChild(midiChoice); | ||||
yPos += midiChoice->box.size.y + margin; | |||||
} | |||||
yPos += 5; | |||||
addOutput(createOutput(Vec(25, yPos), module, MidiInterface::PITCH_OUTPUT)); | |||||
addOutput(createOutput(Vec(75, yPos), module, MidiInterface::GATE_OUTPUT)); | |||||
yPos += 25 + margin; | |||||
{ | |||||
Label *pitchLabel = new Label(); | |||||
pitchLabel->box.pos = Vec(25-12, yPos); | |||||
pitchLabel->text = "Pitch"; | |||||
addChild(pitchLabel); | |||||
Label *gateLabel = new Label(); | |||||
gateLabel->box.pos = Vec(75-12, yPos); | |||||
gateLabel->text = "Gate"; | |||||
addChild(gateLabel); | |||||
yPos += pitchLabel->box.size.y + margin; | |||||
} | } | ||||
} | } | ||||
@@ -4,6 +4,9 @@ | |||||
using namespace rack; | using namespace rack; | ||||
Plugin *coreInit() { | Plugin *coreInit() { | ||||
audioInit(); | |||||
midiInit(); | |||||
Plugin *plugin = createPlugin("Core", "Core"); | Plugin *plugin = createPlugin("Core", "Core"); | ||||
createModel<AudioInterfaceWidget>(plugin, "AudioInterface", "Audio Interface"); | createModel<AudioInterfaceWidget>(plugin, "AudioInterface", "Audio Interface"); | ||||
createModel<MidiInterfaceWidget>(plugin, "MidiInterface", "MIDI Interface"); | createModel<MidiInterfaceWidget>(plugin, "MidiInterface", "MIDI Interface"); | ||||
@@ -3,6 +3,9 @@ | |||||
rack::Plugin *coreInit(); | rack::Plugin *coreInit(); | ||||
void audioInit(); | |||||
void midiInit(); | |||||
//////////////////// | //////////////////// | ||||
// module widgets | // module widgets | ||||
//////////////////// | //////////////////// | ||||
@@ -3,10 +3,6 @@ | |||||
namespace rack { | namespace rack { | ||||
Button::Button() { | |||||
box.size.y = BND_WIDGET_HEIGHT; | |||||
} | |||||
void Button::draw(NVGcontext *vg) { | void Button::draw(NVGcontext *vg) { | ||||
bndToolButton(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, BND_CORNER_NONE, state, -1, text.c_str()); | bndToolButton(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, BND_CORNER_NONE, state, -1, text.c_str()); | ||||
} | } | ||||
@@ -3,11 +3,6 @@ | |||||
namespace rack { | namespace rack { | ||||
ScrollBar::ScrollBar() { | |||||
box.size.x = BND_SCROLLBAR_WIDTH; | |||||
box.size.y = BND_SCROLLBAR_HEIGHT; | |||||
} | |||||
void ScrollBar::draw(NVGcontext *vg) { | void ScrollBar::draw(NVGcontext *vg) { | ||||
float boxSize = (orientation == VERTICAL ? box.size.y : box.size.x); | float boxSize = (orientation == VERTICAL ? box.size.y : box.size.x); | ||||
float maxOffset = containerSize - boxSize; | float maxOffset = containerSize - boxSize; | ||||
@@ -5,10 +5,6 @@ namespace rack { | |||||
#define SLIDER_SENSITIVITY 0.001 | #define SLIDER_SENSITIVITY 0.001 | ||||
Slider::Slider() { | |||||
box.size.y = BND_WIDGET_HEIGHT; | |||||
} | |||||
void Slider::draw(NVGcontext *vg) { | void Slider::draw(NVGcontext *vg) { | ||||
float progress = mapf(value, minValue, maxValue, 0.0, 1.0); | float progress = mapf(value, minValue, maxValue, 0.0, 1.0); | ||||
bndSlider(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, BND_CORNER_NONE, state, progress, getText().c_str(), NULL); | bndSlider(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, BND_CORNER_NONE, state, progress, getText().c_str(), NULL); | ||||