@@ -1,19 +1,19 @@ | |||||
# Rack | # Rack | ||||
*Rack* is the engine for the VCV open-source virtual Eurorack DAW. | |||||
*Rack* is the engine for the VCV open-source virtual modular synthesizer. | |||||
![Rack screenshot](https://vcvrack.com/images/screenshot.png) | ![Rack screenshot](https://vcvrack.com/images/screenshot.png) | ||||
This README includes instructions for building Rack from source. For information about the software, go to https://vcvrack.com/. | This README includes instructions for building Rack from source. For information about the software, go to https://vcvrack.com/. | ||||
## The [Issue Tracker](https://github.com/VCVRack/Rack/issues) *is* the official developer's forum | |||||
## The [Issue Tracker](https://github.com/VCVRack/Rack/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) *is* the official developer's forum | |||||
Bug reports, feature requests, and even *questions/discussions* are welcome on the GitHub Issue Tracker for all VCVRack repos. | Bug reports, feature requests, and even *questions/discussions* are welcome on the GitHub Issue Tracker for all VCVRack repos. | ||||
However, please search before posting to avoid duplicates, and limit to one issue per post. | However, please search before posting to avoid duplicates, and limit to one issue per post. | ||||
You may vote on feature requests by using the Thumbs Up/Down reaction on the first post. | |||||
Please vote on feature requests by using the Thumbs Up/Down reaction on the first post. | |||||
I rarely accept Pull Requests, so please notify me in advance to plan your contribution before writing code. | |||||
I rarely accept code contributions to Rack itself, so please notify me in advance if you wish to send a pull request. | |||||
## Setting up your development environment | ## Setting up your development environment | ||||
@@ -33,14 +33,16 @@ Install build dependencies with the pacman package manger. | |||||
### Linux | ### Linux | ||||
With your distro's package manager, make sure you have installed `git`, `gcc`, `make`, `cmake`, `tar`, and `unzip`. | |||||
With your distro's package manager, make sure you have installed `git`, `gcc`, `make`, `cmake`, `tar`, `unzip`, and `curl`. | |||||
## Building | ## Building | ||||
*If the build fails for you, please report the issue with a detailed error message to help the portability of Rack.* | *If the build fails for you, please report the issue with a detailed error message to help the portability of Rack.* | ||||
Clone this repository with `git clone https://github.com/VCVRack/Rack.git` and `cd Rack`. | Clone this repository with `git clone https://github.com/VCVRack/Rack.git` and `cd Rack`. | ||||
If you would like to build a previous version of Rack instead of the master branch, check out the desired tag with `git checkout v0.4.0` for example. | |||||
The `master` branch contains the latest public code and breaks its plugin [API](https://en.wikipedia.org/wiki/Application_programming_interface) and [ABI](https://en.wikipedia.org/wiki/Application_binary_interface) frequently. | |||||
If you wish to build a previous version of Rack which is API/ABI-compatible with an official Rack release, check out the desired branch with `git checkout v0.5` for example. | |||||
Clone submodules. | Clone submodules. | ||||
@@ -51,7 +53,7 @@ You may use make's `-j$(nproc)` flag to parallelize builds across all your CPU c | |||||
make dep | make dep | ||||
You may use `make dep RTAUDIO_ALL_APIS=1` to attempt to build with all audio driver APIs enabled for your operating system. | |||||
You may use `make dep RTAUDIO_ALL_APIS=1` to attempt to build with all audio driver APIs enabled for your operating system, although this is unsupported. | |||||
You should see a message that all dependencies built successfully. | You should see a message that all dependencies built successfully. | ||||
@@ -12,12 +12,10 @@ namespace rack { | |||||
struct AudioIO { | struct AudioIO { | ||||
int maxOutputs = 8; | |||||
int maxInputs = 8; | |||||
// Stream properties | // Stream properties | ||||
int driver = 0; | int driver = 0; | ||||
int device = -1; | int device = -1; | ||||
int offset = 0; | |||||
int sampleRate = 44100; | int sampleRate = 44100; | ||||
int blockSize = 256; | int blockSize = 256; | ||||
int numOutputs = 0; | int numOutputs = 0; | ||||
@@ -34,8 +32,16 @@ struct AudioIO { | |||||
void setDriver(int driver); | void setDriver(int driver); | ||||
int getDeviceCount(); | int getDeviceCount(); | ||||
bool getDeviceInfo(int device, RtAudio::DeviceInfo *deviceInfo); | |||||
int getDeviceMaxChannels(int device); | |||||
std::string getDeviceName(int device); | std::string getDeviceName(int device); | ||||
std::string getDeviceDetail(int device); | |||||
std::string getDeviceDetail(int device, int offset, int maxChannels); | |||||
void setDevice(int device, int offset, int maxChannels); | |||||
void setSampleRate(int sampleRate); | |||||
void setBlockSize(int blockSize); | |||||
/** Must close the stream before opening */ | |||||
void openStream(); | void openStream(); | ||||
void closeStream(); | void closeStream(); | ||||
/** Returns whether the audio stream is open and running */ | /** Returns whether the audio stream is open and running */ | ||||
@@ -36,14 +36,17 @@ struct AudioDriverChoice : LedDisplayChoice { | |||||
struct AudioDeviceItem : MenuItem { | struct AudioDeviceItem : MenuItem { | ||||
AudioIO *audioIO; | AudioIO *audioIO; | ||||
int device; | int device; | ||||
int offset; | |||||
int maxChannels; | |||||
void onAction(EventAction &e) override { | void onAction(EventAction &e) override { | ||||
audioIO->device = device; | |||||
audioIO->openStream(); | |||||
audioIO->setDevice(device, offset, maxChannels); | |||||
} | } | ||||
}; | }; | ||||
struct AudioDeviceChoice : LedDisplayChoice { | struct AudioDeviceChoice : LedDisplayChoice { | ||||
AudioWidget *audioWidget; | AudioWidget *audioWidget; | ||||
int groupChannels = 8; | |||||
int maxTotalChannels = 64; | |||||
void onAction(EventAction &e) override { | void onAction(EventAction &e) override { | ||||
Menu *menu = gScene->createMenu(); | Menu *menu = gScene->createMenu(); | ||||
menu->addChild(construct<MenuLabel>(&MenuLabel::text, "Audio device")); | menu->addChild(construct<MenuLabel>(&MenuLabel::text, "Audio device")); | ||||
@@ -57,16 +60,21 @@ struct AudioDeviceChoice : LedDisplayChoice { | |||||
menu->addChild(item); | menu->addChild(item); | ||||
} | } | ||||
for (int device = 0; device < deviceCount; device++) { | for (int device = 0; device < deviceCount; device++) { | ||||
AudioDeviceItem *item = new AudioDeviceItem(); | |||||
item->audioIO = audioWidget->audioIO; | |||||
item->device = device; | |||||
item->text = audioWidget->audioIO->getDeviceDetail(device); | |||||
item->rightText = CHECKMARK(item->device == audioWidget->audioIO->device); | |||||
menu->addChild(item); | |||||
int maxChannels = min(maxTotalChannels, audioWidget->audioIO->getDeviceMaxChannels(device)); | |||||
for (int offset = 0; offset < maxChannels; offset += groupChannels) { | |||||
AudioDeviceItem *item = new AudioDeviceItem(); | |||||
item->audioIO = audioWidget->audioIO; | |||||
item->device = device; | |||||
item->offset = offset; | |||||
item->maxChannels = groupChannels; | |||||
item->text = audioWidget->audioIO->getDeviceDetail(device, offset, groupChannels); | |||||
item->rightText = CHECKMARK(item->device == audioWidget->audioIO->device && item->offset == audioWidget->audioIO->offset); | |||||
menu->addChild(item); | |||||
} | |||||
} | } | ||||
} | } | ||||
void step() override { | void step() override { | ||||
text = audioWidget->audioIO->getDeviceDetail(audioWidget->audioIO->device); | |||||
text = audioWidget->audioIO->getDeviceDetail(audioWidget->audioIO->device, audioWidget->audioIO->offset, groupChannels); | |||||
if (text.empty()) { | if (text.empty()) { | ||||
text = "(No device)"; | text = "(No device)"; | ||||
color.a = 0.5f; | color.a = 0.5f; | ||||
@@ -83,8 +91,7 @@ struct AudioSampleRateItem : MenuItem { | |||||
AudioIO *audioIO; | AudioIO *audioIO; | ||||
int sampleRate; | int sampleRate; | ||||
void onAction(EventAction &e) override { | void onAction(EventAction &e) override { | ||||
audioIO->sampleRate = sampleRate; | |||||
audioIO->openStream(); | |||||
audioIO->setSampleRate(sampleRate); | |||||
} | } | ||||
}; | }; | ||||
@@ -112,8 +119,7 @@ struct AudioBlockSizeItem : MenuItem { | |||||
AudioIO *audioIO; | AudioIO *audioIO; | ||||
int blockSize; | int blockSize; | ||||
void onAction(EventAction &e) override { | void onAction(EventAction &e) override { | ||||
audioIO->blockSize = blockSize; | |||||
audioIO->openStream(); | |||||
audioIO->setBlockSize(blockSize); | |||||
} | } | ||||
}; | }; | ||||
@@ -75,54 +75,107 @@ int AudioIO::getDeviceCount() { | |||||
return 0; | return 0; | ||||
} | } | ||||
bool AudioIO::getDeviceInfo(int device, RtAudio::DeviceInfo *deviceInfo) { | |||||
if (!deviceInfo) | |||||
return false; | |||||
if (rtAudio) { | |||||
if (device == this->device) { | |||||
*deviceInfo = this->deviceInfo; | |||||
return true; | |||||
} | |||||
else { | |||||
try { | |||||
*deviceInfo = rtAudio->getDeviceInfo(device); | |||||
return true; | |||||
} | |||||
catch (RtAudioError &e) { | |||||
warn("Failed to query RtAudio device: %s", e.what()); | |||||
return false; | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
return false; | |||||
} | |||||
} | |||||
int AudioIO::getDeviceMaxChannels(int device) { | |||||
if (device < 0) | |||||
return 0; | |||||
if (rtAudio) { | |||||
RtAudio::DeviceInfo deviceInfo; | |||||
if (getDeviceInfo(device, &deviceInfo)) | |||||
return max(deviceInfo.inputChannels, deviceInfo.outputChannels); | |||||
} | |||||
if (driver == BRIDGE_DRIVER) { | |||||
return 2; | |||||
} | |||||
return 0; | |||||
} | |||||
std::string AudioIO::getDeviceName(int device) { | std::string AudioIO::getDeviceName(int device) { | ||||
if (device < 0) | if (device < 0) | ||||
return ""; | return ""; | ||||
if (rtAudio) { | if (rtAudio) { | ||||
try { | |||||
RtAudio::DeviceInfo deviceInfo; | |||||
if (device == this->device) | |||||
deviceInfo = this->deviceInfo; | |||||
else | |||||
deviceInfo = rtAudio->getDeviceInfo(device); | |||||
RtAudio::DeviceInfo deviceInfo; | |||||
if (getDeviceInfo(device, &deviceInfo)) | |||||
return deviceInfo.name; | return deviceInfo.name; | ||||
} | |||||
catch (RtAudioError &e) { | |||||
warn("Failed to query RtAudio device: %s", e.what()); | |||||
} | |||||
} | } | ||||
if (driver == BRIDGE_DRIVER) { | if (driver == BRIDGE_DRIVER) { | ||||
if (device >= 0) | |||||
return stringf("%d", device + 1); | |||||
return stringf("%d", device + 1); | |||||
} | } | ||||
return ""; | return ""; | ||||
} | } | ||||
std::string AudioIO::getDeviceDetail(int device) { | |||||
std::string AudioIO::getDeviceDetail(int device, int offset, int maxChannels) { | |||||
if (device < 0) | if (device < 0) | ||||
return ""; | return ""; | ||||
if (rtAudio) { | if (rtAudio) { | ||||
try { | |||||
RtAudio::DeviceInfo deviceInfo; | |||||
if (device == this->device) | |||||
deviceInfo = this->deviceInfo; | |||||
else | |||||
deviceInfo = rtAudio->getDeviceInfo(device); | |||||
return stringf("%s (%d in, %d out)", deviceInfo.name.c_str(), deviceInfo.inputChannels, deviceInfo.outputChannels); | |||||
} | |||||
catch (RtAudioError &e) { | |||||
warn("Failed to query RtAudio device: %s", e.what()); | |||||
RtAudio::DeviceInfo deviceInfo; | |||||
if (getDeviceInfo(device, &deviceInfo)) { | |||||
std::string deviceDetail = stringf("%s (", deviceInfo.name.c_str()); | |||||
if (offset < (int) deviceInfo.inputChannels) | |||||
deviceDetail += stringf("%d-%d in", offset + 1, min(offset + maxChannels, deviceInfo.inputChannels)); | |||||
if (offset < (int) deviceInfo.inputChannels && offset < (int) deviceInfo.outputChannels) | |||||
deviceDetail += ", "; | |||||
if (offset < (int) deviceInfo.outputChannels) | |||||
deviceDetail += stringf("%d-%d out", offset + 1, min(offset + maxChannels, deviceInfo.outputChannels)); | |||||
deviceDetail += ")"; | |||||
return deviceDetail; | |||||
} | } | ||||
} | } | ||||
if (driver == BRIDGE_DRIVER) { | if (driver == BRIDGE_DRIVER) { | ||||
if (device >= 0) | |||||
return stringf("Channel %d", device + 1); | |||||
return stringf("Channel %d", device + 1); | |||||
} | } | ||||
return ""; | return ""; | ||||
} | } | ||||
void AudioIO::setDevice(int device, int offset, int maxChannels) { | |||||
closeStream(); | |||||
this->device = device; | |||||
this->offset = offset; | |||||
this->numOutputs = maxChannels; | |||||
this->numInputs = maxChannels; | |||||
openStream(); | |||||
} | |||||
void AudioIO::setSampleRate(int sampleRate) { | |||||
closeStream(); | |||||
this->sampleRate = sampleRate; | |||||
openStream(); | |||||
} | |||||
void AudioIO::setBlockSize(int blockSize) { | |||||
closeStream(); | |||||
this->blockSize = blockSize; | |||||
openStream(); | |||||
} | |||||
static int rtCallback(void *outputBuffer, void *inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status, void *userData) { | static int rtCallback(void *outputBuffer, void *inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status, void *userData) { | ||||
AudioIO *audioIO = (AudioIO*) userData; | AudioIO *audioIO = (AudioIO*) userData; | ||||
assert(audioIO); | assert(audioIO); | ||||
@@ -131,10 +184,6 @@ static int rtCallback(void *outputBuffer, void *inputBuffer, unsigned int nFrame | |||||
} | } | ||||
void AudioIO::openStream() { | void AudioIO::openStream() { | ||||
// Close device but remember the current device number | |||||
int device = this->device; | |||||
closeStream(); | |||||
if (device < 0) | if (device < 0) | ||||
return; | return; | ||||
@@ -148,8 +197,11 @@ void AudioIO::openStream() { | |||||
return; | return; | ||||
} | } | ||||
numOutputs = min(deviceInfo.outputChannels, maxOutputs); | |||||
numInputs = min(deviceInfo.inputChannels, maxInputs); | |||||
if (rtAudio->isStreamOpen()) | |||||
return; | |||||
numOutputs = clamp((int) deviceInfo.outputChannels - offset, 0, numOutputs); | |||||
numInputs = clamp((int) deviceInfo.inputChannels - offset, 0, numInputs); | |||||
if (numOutputs == 0 && numInputs == 0) { | if (numOutputs == 0 && numInputs == 0) { | ||||
warn("RtAudio device %d has 0 inputs and 0 outputs"); | warn("RtAudio device %d has 0 inputs and 0 outputs"); | ||||
@@ -159,10 +211,12 @@ void AudioIO::openStream() { | |||||
RtAudio::StreamParameters outParameters; | RtAudio::StreamParameters outParameters; | ||||
outParameters.deviceId = device; | outParameters.deviceId = device; | ||||
outParameters.nChannels = numOutputs; | outParameters.nChannels = numOutputs; | ||||
outParameters.firstChannel = offset; | |||||
RtAudio::StreamParameters inParameters; | RtAudio::StreamParameters inParameters; | ||||
inParameters.deviceId = device; | inParameters.deviceId = device; | ||||
inParameters.nChannels = numInputs; | inParameters.nChannels = numInputs; | ||||
inParameters.firstChannel = offset; | |||||
RtAudio::StreamOptions options; | RtAudio::StreamOptions options; | ||||
// options.flags |= RTAUDIO_SCHEDULE_REALTIME; | // options.flags |= RTAUDIO_SCHEDULE_REALTIME; | ||||
@@ -175,11 +229,12 @@ void AudioIO::openStream() { | |||||
} | } | ||||
try { | try { | ||||
debug("Opening audio RtAudio device %d", device); | |||||
debug("Opening audio RtAudio device %d with %d in %d out", device, numInputs, numOutputs); | |||||
rtAudio->openStream( | rtAudio->openStream( | ||||
numOutputs == 0 ? NULL : &outParameters, | numOutputs == 0 ? NULL : &outParameters, | ||||
numInputs == 0 ? NULL : &inParameters, | numInputs == 0 ? NULL : &inParameters, | ||||
RTAUDIO_FLOAT32, closestSampleRate, (unsigned int*) &blockSize, &rtCallback, this, &options, NULL); | |||||
RTAUDIO_FLOAT32, closestSampleRate, (unsigned int*) &blockSize, | |||||
&rtCallback, this, &options, NULL); | |||||
} | } | ||||
catch (RtAudioError &e) { | catch (RtAudioError &e) { | ||||
warn("Failed to open RtAudio stream: %s", e.what()); | warn("Failed to open RtAudio stream: %s", e.what()); | ||||
@@ -197,12 +252,11 @@ void AudioIO::openStream() { | |||||
// Update sample rate because this may have changed | // Update sample rate because this may have changed | ||||
this->sampleRate = rtAudio->getStreamSampleRate(); | this->sampleRate = rtAudio->getStreamSampleRate(); | ||||
this->device = device; | |||||
onOpenStream(); | onOpenStream(); | ||||
} | } | ||||
if (driver == BRIDGE_DRIVER) { | if (driver == BRIDGE_DRIVER) { | ||||
if (device < BRIDGE_CHANNELS) { | if (device < BRIDGE_CHANNELS) { | ||||
this->device = device; | |||||
// TODO | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -230,10 +284,6 @@ void AudioIO::closeStream() { | |||||
deviceInfo = RtAudio::DeviceInfo(); | deviceInfo = RtAudio::DeviceInfo(); | ||||
} | } | ||||
// Reset rtAudio settings | |||||
device = -1; | |||||
numOutputs = 0; | |||||
numInputs = 0; | |||||
onCloseStream(); | onCloseStream(); | ||||
} | } | ||||
@@ -274,6 +324,8 @@ json_t *AudioIO::toJson() { | |||||
} | } | ||||
void AudioIO::fromJson(json_t *rootJ) { | void AudioIO::fromJson(json_t *rootJ) { | ||||
closeStream(); | |||||
json_t *driverJ = json_object_get(rootJ, "driver"); | json_t *driverJ = json_object_get(rootJ, "driver"); | ||||
if (driverJ) | if (driverJ) | ||||
setDriver(json_number_value(driverJ)); | setDriver(json_number_value(driverJ)); | ||||
@@ -35,11 +35,10 @@ struct AudioInterfaceIO : AudioIO { | |||||
DoubleRingBuffer<Frame<OUTPUTS>, (1<<15)> outputBuffer; | DoubleRingBuffer<Frame<OUTPUTS>, (1<<15)> outputBuffer; | ||||
AudioInterfaceIO() { | AudioInterfaceIO() { | ||||
maxOutputs = OUTPUTS; | |||||
maxInputs = INPUTS; | |||||
} | } | ||||
~AudioInterfaceIO() { | ~AudioInterfaceIO() { | ||||
// Close stream here before destructing AudioInterfaceIO, so the mutexes are still valid when waiting to close. | |||||
closeStream(); | closeStream(); | ||||
} | } | ||||