| @@ -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. | |||||
|  |  | ||||
| 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(); | ||||
| } | } | ||||