diff --git a/.gitignore b/.gitignore index 73b5548..bff5f82 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,7 @@ Testing # clion cmake builds cmake-build-* -build \ No newline at end of file +build + +juce_build +test/output \ No newline at end of file diff --git a/Source/PluginProcess.cpp b/Source/PluginProcess.cpp index a0ec51a..21502d7 100644 --- a/Source/PluginProcess.cpp +++ b/Source/PluginProcess.cpp @@ -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 @@ -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& audioInputFileReaders, - const std::optional& outputChannelCountOpt, - unsigned int& totalNumInputChannelsOut, unsigned int& totalNumOutputChannelsOut + const std::optional& 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; } @@ -115,7 +105,8 @@ ParameterAutomation parseParameters( if (parameterFileOpt) { automation = Automation::parseAutomationDefinition( parameterFileOpt->loadFileAsString().toStdString(), plugin, sampleRate, - inputLengthInSamples); + inputLengthInSamples + ); } // parse command-line supplied parameters @@ -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); @@ -145,7 +138,7 @@ ParameterAutomation parseParameters( << std::endl; } - automation[paramName] = AutomationKeyframes({{0, normalizedValue}}); + automation[paramName] = AutomationKeyframes({ { 0, normalizedValue } }); } return automation; diff --git a/Source/PluginProcess.h b/Source/PluginProcess.h index 703af1e..1f99b21 100644 --- a/Source/PluginProcess.h +++ b/Source/PluginProcess.h @@ -5,7 +5,6 @@ #include #include - /** * Creates readers for the given audio files, verifying that their sample rate matches. * @@ -29,24 +28,21 @@ createAudioFileReaders(const std::vector& 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& audioInputFileReaders, - const std::optional& outputChannelCountOpt, - unsigned int& totalNumInputChannelsOut, unsigned int& totalNumOutputChannelsOut + const std::optional& outputChannelCountOpt ); /** diff --git a/Source/Utils.cpp b/Source/Utils.cpp index 043a260..c797d0a 100644 --- a/Source/Utils.cpp +++ b/Source/Utils.cpp @@ -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; diff --git a/Source/Utils.h b/Source/Utils.h index 3889202..da754af 100644 --- a/Source/Utils.h +++ b/Source/Utils.h @@ -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); @@ -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. diff --git a/Source/commands/ProcessCommand.cpp b/Source/commands/ProcessCommand.cpp index 2fbb148..aaca195 100644 --- a/Source/commands/ProcessCommand.cpp +++ b/Source/commands/ProcessCommand.cpp @@ -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 +#include +#include + +// 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 ProcessCommand::createApp() { // don't break these lines, please @@ -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 @@ -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 outWriter; outputFilePath.deleteFile(); - if (std::unique_ptr outputStream{ outputFilePath.createOutputStream(static_cast(blockSize)) }) { + if (std::unique_ptr outputStream{ + outputFilePath.createOutputStream(static_cast(blockSize)) }) { juce::WavAudioFormat outFormat; outWriter = outFormat.createWriterFor( outputStream, // stream is now managed by writer @@ -112,13 +140,15 @@ void ProcessCommand::execute() { .withBitsPerSample(static_cast(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 sampleBuffer( - (int) std::max(totalNumInputChannels, totalNumOutputChannels), (int) blockSize); + (int) std::max(totalNumInputChannels, totalNumOutputChannels), (int) blockSize + ); juce::MidiBuffer midiBuffer; size_t sampleIndex = 0; @@ -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(sampleIndex), - (int) blockSize)) { + if (!inputFileReader->read( + sampleBuffer.getArrayOfWritePointers() + targetChannel, + (int) inputFileReader->numChannels, static_cast(sampleIndex), + (int) blockSize + )) { throw CLIException("Error reading input file"); // TODO: more context, which file? } @@ -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(blockSize)) { + if (timestampSamples >= sampleIndex && + timestampSamples < sampleIndex + static_cast(blockSize)) { midiBuffer.addEvent(meh->message, (int) (timestampSamples - sampleIndex)); } } @@ -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(blockSize); diff --git a/test/PlugalyzeeAudio/PluginProcessor.cpp b/test/PlugalyzeeAudio/PluginProcessor.cpp index 908264f..ec65efb 100644 --- a/test/PlugalyzeeAudio/PluginProcessor.cpp +++ b/test/PlugalyzeeAudio/PluginProcessor.cpp @@ -1,6 +1,7 @@ #include "PluginProcessor.h" #include "PlugalyzeeAudio.h" +#include #include #include #include @@ -13,6 +14,9 @@ PlugalyzeeAudioProcessor::PlugalyzeeAudioProcessor() : BusesProperties() .withInput ("Input", juce::AudioChannelSet::stereo(), true) .withOutput ("Output", juce::AudioChannelSet::stereo(), true) +#ifdef PLUGALYZEE_HAS_SIDECHAIN + .withInput("Sidechain", juce::AudioChannelSet::stereo(), true) +#endif ), state(*this, nullptr, id::apvtsRoot, createParameterLayout()) // clang-format on @@ -101,11 +105,10 @@ void PlugalyzeeAudioProcessor::changeProgramName(int index, const juce::String& void PlugalyzeeAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock) { DBG(std::format("Prepare to play({},{})", sampleRate, samplesPerBlock)); - jassert(getTotalNumInputChannels() == getTotalNumOutputChannels()); dsp::ProcessSpec spec{ .sampleRate = sampleRate, .maximumBlockSize = static_cast(samplesPerBlock), - .numChannels = static_cast(getTotalNumInputChannels()) + .numChannels = static_cast(getTotalNumOutputChannels()) }; processor.prepare(spec); } @@ -116,21 +119,46 @@ void PlugalyzeeAudioProcessor::releaseResources() bool PlugalyzeeAudioProcessor::isBusesLayoutSupported(const BusesLayout& layouts) const { -#if JucePlugin_IsMidiEffect - juce::ignoreUnused(layouts); - return true; -#else - if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono() - && layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) + const auto candidateInputBuses = layouts.getBuses(true); + const auto candidateOutputBuses = layouts.getBuses(false); + + auto describeLayout = [&]() { + juce::String result; + result << std::format("Input buses: {}, output buses: {}\n", candidateInputBuses.size(), candidateOutputBuses.size()); + + result << "Input buses:"; + for (auto& bus : candidateInputBuses) + { + result << bus.getDescription() << ","; + } + + result << "\n"; + + result << "Output buses:"; + for (auto& bus : candidateOutputBuses) + { + result << bus.getDescription() << ","; + } + + return result; + }; + + DBG(describeLayout()); + +#ifdef PLUGALYZEE_HAS_SIDECHAIN + auto buses = layouts.getBuses(true); + + if (buses.size() < 2) + { return false; + } +#endif - #if !JucePlugin_IsSynth - if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet()) + if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono() + && layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) return false; - #endif return true; -#endif } void PlugalyzeeAudioProcessor::processBlock( @@ -152,7 +180,8 @@ void PlugalyzeeAudioProcessor::processBlock( }); dsp::AudioBlock block{ buffer }; - dsp::ProcessContextReplacing context{ block }; + auto blockToProcess = block.getSubsetChannelBlock(0, static_cast(getTotalNumOutputChannels())); + dsp::ProcessContextReplacing context{ blockToProcess }; processor.process(context); } diff --git a/test/PlugalyzeeAudio/preset1.json b/test/PlugalyzeeAudio/preset1.json new file mode 100644 index 0000000..199946a --- /dev/null +++ b/test/PlugalyzeeAudio/preset1.json @@ -0,0 +1,5 @@ +{ + "In Gain": "6.0", + "Ratio": "1:20", + "Threshold": "-18" +} \ No newline at end of file