Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@ Testing

# clion cmake builds
cmake-build-*
build
build

juce_build
test/output
63 changes: 28 additions & 35 deletions Source/PluginProcess.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#include "Automation.h"
#include "PluginProcess.h"
#include "Parsers.h"

#include "Automation.h"
#include "Errors.h"
#include "Parsers.h"
#include "Utils.h"

#include <CLI/CLI.hpp>
Expand Down Expand Up @@ -70,37 +71,26 @@ juce::MidiFile readMIDIFile(const juce::File& file, double sampleRate, size_t& l
juce::AudioPluginInstance::BusesLayout createBusLayout(
const juce::AudioPluginInstance& plugin,
const juce::OwnedArray<juce::AudioFormatReader>& audioInputFileReaders,
const std::optional<unsigned int>& outputChannelCountOpt,
unsigned int& totalNumInputChannelsOut, unsigned int& totalNumOutputChannelsOut
const std::optional<unsigned int>& outputChannelCount
) {

totalNumInputChannelsOut = 0;
juce::AudioPluginInstance::BusesLayout layout;
if (audioInputFileReaders.isEmpty()) {
// if no input files are provided, use the plugin's default input bus layout
// to maximize compatibility with synths that expect an input
layout.inputBuses = plugin.getBusesLayout().inputBuses;
for (auto& cs : layout.inputBuses) {
totalNumInputChannelsOut += (unsigned int) cs.size();
}

} else {
for (auto* inputFileReader : audioInputFileReaders) {
layout.inputBuses.add(
juce::AudioChannelSet::canonicalChannelSet((int) inputFileReader->numChannels));
totalNumInputChannelsOut += inputFileReader->numChannels;
}
// Start with compatible bus layout
juce::AudioPluginInstance::BusesLayout layout{ plugin.getBusesLayout() };
jassert(layout.outputBuses.size() == 1);

// Inputs: Change channel formats to match input files
for (const auto& [index, reader] : juce::enumerate(audioInputFileReaders)) {
layout.inputBuses[index] =
juce::AudioChannelSet::canonicalChannelSet((int) reader->numChannels);
}

// create an output bus with the desired amount of channels,
// defaulting to the same amount of channels as the main input file if one exists,
// or the plugin's default bus layout otherwise.
totalNumOutputChannelsOut = (outputChannelCountOpt.value_or(
audioInputFileReaders.isEmpty()
? (unsigned int) plugin.getBusesLayout().getMainOutputChannels()
: audioInputFileReaders[0]->numChannels));
layout.outputBuses.add(
juce::AudioChannelSet::canonicalChannelSet((int) totalNumOutputChannelsOut));
// Outputs: Change channel formats to match user-supplied option or first input file
if (outputChannelCount.has_value()) {
layout.outputBuses[0] =
juce::AudioChannelSet::canonicalChannelSet((int) *outputChannelCount);
} else if (!audioInputFileReaders.isEmpty()) {
layout.outputBuses[0] =
juce::AudioChannelSet::canonicalChannelSet((int) audioInputFileReaders[0]->numChannels);
}

return layout;
}
Expand All @@ -115,7 +105,8 @@ ParameterAutomation parseParameters(
if (parameterFileOpt) {
automation = Automation::parseAutomationDefinition(
parameterFileOpt->loadFileAsString().toStdString(), plugin, sampleRate,
inputLengthInSamples);
inputLengthInSamples
);
}

// parse command-line supplied parameters
Expand All @@ -129,9 +120,11 @@ ParameterAutomation parseParameters(

if (!isNormalizedValue) {
if (!Automation::parameterSupportsTextToValueConversion(param)) {
throw CLIException("Parameter '" + paramName +
"' does not support text values. Use :n suffix to supply "
"a normalized value instead");
throw CLIException(
"Parameter '" + paramName +
"' does not support text values. Use :n suffix to supply "
"a normalized value instead"
);
}

normalizedValue = param->getValueForText(textValue);
Expand All @@ -145,7 +138,7 @@ ParameterAutomation parseParameters(
<< std::endl;
}

automation[paramName] = AutomationKeyframes({{0, normalizedValue}});
automation[paramName] = AutomationKeyframes({ { 0, normalizedValue } });
}

return automation;
Expand Down
18 changes: 7 additions & 11 deletions Source/PluginProcess.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
#include <juce_audio_formats/juce_audio_formats.h>
#include <juce_audio_processors/juce_audio_processors.h>


/**
* Creates readers for the given audio files, verifying that their sample rate matches.
*
Expand All @@ -29,24 +28,21 @@ createAudioFileReaders(const std::vector<juce::File>& files, size_t& maxLengthIn
juce::MidiFile readMIDIFile(const juce::File& file, double sampleRate, size_t& lengthInSamplesOut);

/**
* Creates a bus layout with one input bus for each input file.
* Creates a bus layout according to the user input.
*
* The number of input buses will match the plugin, and their channel sets will match the input
* files. The number of output buses will be 1, and it's channel set will match (in order of
* preference) the user-supplied number, the first input file, or the plugin's default.
*
* @param plugin The plugin to create the bus layout for.
* @param audioInputFileReaders The file readers for each input file.
* @param outputChannelCountOpt The amount of channels to use for the output bus. If not
* supplied, the same amount of channels as the main input file is used. If no such file exists,
* the plugin's default bus layout is used.
* @param totalNumInputChannelsOut Variable that will be populated with the total amount of
* input channels the returned layout has.
* @param totalNumOutputChannelsOut Variable that will be populated with the total amount of
* output channels the returned layout has.
* @param outputChannelCountOpt The user-supplied number of channels to use for the output bus.
* @return The bus layout.
*/
juce::AudioPluginInstance::BusesLayout createBusLayout(
const juce::AudioPluginInstance& plugin,
const juce::OwnedArray<juce::AudioFormatReader>& audioInputFileReaders,
const std::optional<unsigned int>& outputChannelCountOpt,
unsigned int& totalNumInputChannelsOut, unsigned int& totalNumOutputChannelsOut
const std::optional<unsigned int>& outputChannelCountOpt
);

/**
Expand Down
50 changes: 50 additions & 0 deletions Source/Utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,56 @@ std::string getBusLayoutHumanReadable(const nlohmann::json& layoutJson) {
return result;
}

int getTotalNumInputChannels(const juce::AudioProcessor::BusesLayout &layout) {
const auto inputBuses = layout.getBuses(true);

int totalNumInputChannels{ 0 };

for (auto& bus : inputBuses)
{
totalNumInputChannels += bus.size();
}
return totalNumInputChannels;
}

int getTotalNumOutputChannels(const juce::AudioProcessor::BusesLayout &layout) {
const auto outputBuses = layout.getBuses(false);

int totalNumOutputChannels{ 0 };

for (auto& bus : outputBuses)
{
totalNumOutputChannels += bus.size();
}
return totalNumOutputChannels;
}

juce::String describeBusesLayout(const juce::AudioProcessor::BusesLayout& layout) {
const auto inputBuses = layout.getBuses(true);
const auto outputBuses = layout.getBuses(false);

juce::String result;
result << std::format("Input buses: {}, output buses: {}\n", inputBuses.size(), outputBuses.size());

result << "Input buses:";
for (auto& bus : inputBuses)
{
result << bus.getDescription() << ",";
}

result << "\n";

result << "Output buses:";
for (auto& bus : outputBuses)
{
result << bus.getDescription() << ",";
}

result << "\n";

return result;
}

void outputResult(const std::string& text, juce::File outPath, bool overwrite) {
if (outPath == juce::File{}) {
std::cout << text;
Expand Down
32 changes: 31 additions & 1 deletion Source/Utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class PluginUtils {
* @param plugin The plugin.
* @param statePath The path to the binary state
* @param state Memory into which to write the state
* @throws FileLoadError If the state file couldn't be opened.
* @throws FileLoadError If the state file couldn't be opened.
*/
void loadPluginStateFromFile(juce::AudioPluginInstance& plugin, const juce::File& statePath, juce::MemoryBlock& state);

Expand Down Expand Up @@ -106,6 +106,36 @@ nlohmann::json getBusLayoutJson(const juce::AudioProcessor::BusesLayout& layout)
*/
std::string getBusLayoutHumanReadable(const nlohmann::json& layoutJson);

/**
* Find out the total number of input channels across all buses in order to make the right sized
* AudioBuffer
*
* @param layout The buses layout
* @return The total number of input channels
*/
int getTotalNumInputChannels(const juce::AudioProcessor::BusesLayout& layout);

/**
* Find out the total number of output channels across all buses in order to make the right sized
* AudioBuffer
*
* @param layout The buses layout
* @return The total number of input channels
*/
int getTotalNumOutputChannels(const juce::AudioProcessor::BusesLayout& layout);

/**
* Get info about a BusesLayout
* - Total count of input buses
* - Total count of output buses
* - Description of each input bus
* - Description of each output bus
*
* @param layout BusesLayout
* @return String with description
*/
juce::String describeBusesLayout(const juce::AudioProcessor::BusesLayout& layout);

/**
* Outputs text either to stdout or to a file.
* Used for outputting the final result of a command according to the options set by the user.
Expand Down
68 changes: 51 additions & 17 deletions Source/commands/ProcessCommand.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
#include "ProcessCommand.h"
#include "PluginProcess.h"

#include "PluginProcess.h"
#include "PresetLoadingExtensionsVisitor.h"
#include "Parsers.h"
#include "Utils.h"
#include "Validators.h"


#include <cstdio>
#include <juce_audio_processors/juce_audio_processors.h>
#include <print>

// Let the user know if we'll be creating input buses for them
static void
checkBusCountsAndWarn(const juce::AudioPluginInstance& plugin, const auto& audioInputFileReaders) {
// The plugin's default number of inputs and outputs
auto pluginInputBusCount = plugin.getBusCount(true);
// The user-supplied number of inputs and outputs
auto userInputBusCount = audioInputFileReaders.size();
if (pluginInputBusCount != userInputBusCount) {
std::println(
stderr,
"Plugin requires {} input buses. {} input buses provided. "
"Other input buses will be silent.",
pluginInputBusCount, userInputBusCount
);
}
}

std::shared_ptr<CLI::App> ProcessCommand::createApp() {
// don't break these lines, please
Expand Down Expand Up @@ -82,11 +100,18 @@ void ProcessCommand::execute() {
}

// create and apply the bus layout
unsigned int totalNumInputChannels, totalNumOutputChannels;
auto layout = createBusLayout(*plugin, audioInputFileReaders, outputChannelCountOpt,
totalNumInputChannels, totalNumOutputChannels);
const auto pluginOutputBusCount = plugin->getBusCount(false);
if (pluginOutputBusCount != 1) {
throw CLIException("Multi-output plugins currently not supported. Please write a PR!");
}
checkBusCountsAndWarn(*plugin, audioInputFileReaders);

auto layout = createBusLayout(*plugin, audioInputFileReaders, outputChannelCountOpt);

if (!plugin->setBusesLayout(layout)) {
throw CLIException("Plugin does not support requested bus layout");
throw CLIException(
"Plugin does not support requested bus layout: " + describeBusesLayout(layout)
);
}

// parse plugin parameters
Expand All @@ -100,9 +125,12 @@ void ProcessCommand::execute() {
throw CLIException("Output file already exists! Use --overwrite to overwrite the file");
}

auto totalNumInputChannels = getTotalNumInputChannels(layout);
auto totalNumOutputChannels = getTotalNumOutputChannels(layout);
std::unique_ptr<juce::AudioFormatWriter> outWriter;
outputFilePath.deleteFile();
if (std::unique_ptr<juce::OutputStream> outputStream{ outputFilePath.createOutputStream(static_cast<size_t>(blockSize)) }) {
if (std::unique_ptr<juce::OutputStream> outputStream{
outputFilePath.createOutputStream(static_cast<size_t>(blockSize)) }) {
juce::WavAudioFormat outFormat;
outWriter = outFormat.createWriterFor(
outputStream, // stream is now managed by writer
Expand All @@ -112,13 +140,15 @@ void ProcessCommand::execute() {
.withBitsPerSample(static_cast<int>(bitDepth))
);
} else {
throw CLIException("Could not create output stream to write to file " +
outputFilePath.getFullPathName());
throw CLIException(
"Could not create output stream to write to file " + outputFilePath.getFullPathName()
);
}

// process the input files with the plugin
juce::AudioBuffer<float> sampleBuffer(
(int) std::max(totalNumInputChannels, totalNumOutputChannels), (int) blockSize);
(int) std::max(totalNumInputChannels, totalNumOutputChannels), (int) blockSize
);

juce::MidiBuffer midiBuffer;
size_t sampleIndex = 0;
Expand All @@ -129,9 +159,11 @@ void ProcessCommand::execute() {
// read next segment of audio input files into buffer
unsigned int targetChannel = 0;
for (auto* inputFileReader : audioInputFileReaders) {
if (!inputFileReader->read(sampleBuffer.getArrayOfWritePointers() + targetChannel,
(int) inputFileReader->numChannels, static_cast<juce::int64>(sampleIndex),
(int) blockSize)) {
if (!inputFileReader->read(
sampleBuffer.getArrayOfWritePointers() + targetChannel,
(int) inputFileReader->numChannels, static_cast<juce::int64>(sampleIndex),
(int) blockSize
)) {
throw CLIException("Error reading input file"); // TODO: more context, which file?
}

Expand All @@ -149,7 +181,8 @@ void ProcessCommand::execute() {

for (auto& meh : *midiTrack) {
auto timestampSamples = secondsToSamples(meh->message.getTimeStamp(), sampleRate);
if (timestampSamples >= sampleIndex && timestampSamples < sampleIndex + static_cast<size_t>(blockSize)) {
if (timestampSamples >= sampleIndex &&
timestampSamples < sampleIndex + static_cast<size_t>(blockSize)) {
midiBuffer.addEvent(meh->message, (int) (timestampSamples - sampleIndex));
}
}
Expand All @@ -170,8 +203,9 @@ void ProcessCommand::execute() {

// write to output
if (startSample < blockSize) {
outWriter->writeFromAudioSampleBuffer(sampleBuffer, startSample,
(int) blockSize - startSample);
outWriter->writeFromAudioSampleBuffer(
sampleBuffer, startSample, (int) blockSize - startSample
);
}

sampleIndex += static_cast<size_t>(blockSize);
Expand Down
Loading
Loading