NeuralPi

Raspberry Pi guitar pedal using neural networks to emulate real amps and effects
Log | Files | Refs | Submodules | README

commit 7b211c46d34f03a447fffeae72c06b3c57ee86ff
parent 1e888909f5f46aa4e30489e66adecde57e40785c
Author: Keith Bloemer <32459398+GuitarML@users.noreply.github.com>
Date:   Tue, 10 Aug 2021 11:45:13 -0500

Merge pull request #10 from GuitarML/ir-loader

Ir loader
Diffstat:
MNeuralPi.jucer | 1+
MSource/AmpOSCReceiver.h | 12++++++++++++
ASource/CabSim.h | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MSource/PluginEditor.cpp | 223++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
MSource/PluginEditor.h | 13++++++++++++-
MSource/PluginProcessor.cpp | 121++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
MSource/PluginProcessor.h | 31+++++++++++++++++++++++--------
Aresources/cassette_recorder.wav | 0
Aresources/guitar_amp.wav | 0
Mresources/npi_background.jpg | 0
Mscripts/update_models.bat | 21+++++++++++++++------
Mscripts/update_models.sh | 11++++++++++-
12 files changed, 422 insertions(+), 71 deletions(-)

diff --git a/NeuralPi.jucer b/NeuralPi.jucer @@ -16,6 +16,7 @@ <GROUP id="{70CE292C-E9C5-C029-B95A-F7DF41E5F74C}" name="Source"> <FILE id="VgCJPH" name="AmpOSCReceiver.h" compile="0" resource="0" file="Source/AmpOSCReceiver.h"/> + <FILE id="HvJFu8" name="CabSim.h" compile="0" resource="0" file="Source/CabSim.h"/> <FILE id="s1HQuK" name="Eq4Band.cpp" compile="1" resource="0" file="Source/Eq4Band.cpp"/> <FILE id="xtLEtv" name="Eq4Band.h" compile="0" resource="0" file="Source/Eq4Band.h"/> <FILE id="hNjQV9" name="PluginEditor.cpp" compile="1" resource="0" diff --git a/Source/AmpOSCReceiver.h b/Source/AmpOSCReceiver.h @@ -49,6 +49,11 @@ public: return modelValue; } + Value& getIrValue() + { + return irValue; + } + void changePort (int port) { if (! connect (port)) @@ -84,6 +89,7 @@ private: trebleAddressPattern = "/parameter/" + ampName + "/Treble"; presenceAddressPattern = "/parameter/" + ampName + "/Presence"; modelAddressPattern = "/parameter/" + ampName + "/Model"; + irAddressPattern = "/parameter/" + ampName + "/Ir"; } void oscMessageReceived(const OSCMessage& message) override @@ -124,6 +130,10 @@ private: { modelValue.setValue(jlimit(0.0f, 1.0f, message[0].getFloat32())); } + else if (message.getAddressPattern().matches(irAddressPattern)) + { + irValue.setValue(jlimit(0.0f, 1.0f, message[0].getFloat32())); + } } } @@ -138,6 +148,7 @@ private: String trebleAddressPattern {"/parameter/elk_juce_example/Treble"}; String presenceAddressPattern {"/parameter/elk_juce_example/Presence"}; String modelAddressPattern {"/parameter/elk_juce_example/Model"}; + String irAddressPattern {"/parameter/elk_juce_example/Ir"}; Value gainValue {0.5f}; Value masterValue {0.5f}; @@ -147,6 +158,7 @@ private: Value presenceValue {0.5f}; Value modelValue {0.0f}; + Value irValue {0.0f}; bool connected = false; diff --git a/Source/CabSim.h b/Source/CabSim.h @@ -0,0 +1,59 @@ +/* + ============================================================================== + + CabSim + + ============================================================================== +*/ +#include "../JuceLibraryCode/JuceHeader.h" + +#pragma once + +//============================================================================== +class CabSim +{ +public: + //============================================================================== + CabSim() + { + + } + + //============================================================================== + void prepare (const juce::dsp::ProcessSpec& spec) + { + processorChain.prepare(spec); + } + + //============================================================================== + template <typename ProcessContext> + void process(const ProcessContext& context) noexcept + { + processorChain.process(context); + } + + //============================================================================== + void reset() noexcept + { + processorChain.reset(); + } + + void load(File irFile) noexcept + { + auto& convolution = processorChain.template get<convolutionIndex>(); + convolution.loadImpulseResponse(irFile, + juce::dsp::Convolution::Stereo::yes, + juce::dsp::Convolution::Trim::no, + 1024); + } + +private: + enum + { + convolutionIndex + }; + + juce::dsp::ProcessorChain<juce::dsp::Convolution> processorChain; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CabSim) +}; +\ No newline at end of file diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp @@ -80,7 +80,74 @@ NeuralPiAudioProcessorEditor::NeuralPiAudioProcessorEditor (NeuralPiAudioProcess loadButton.setColour(juce::Label::textColourId, juce::Colours::black); loadButton.addListener(this); - //gainSliderAttach = std::make_unique<AudioProcessorValueTreeState::SliderAttachment>(processor.treeState, GAIN_ID, ampGainKnob); + + //addAndMakeVisible(irKnob); + //irKnob.setLookAndFeel(&ampSilverKnobLAF); + irKnob.setTextBoxStyle(juce::Slider::TextEntryBoxPosition::TextBoxBelow, false, 50, 20); + irKnob.setNumDecimalPlacesToDisplay(1); + irKnob.addListener(this); + //irKnob.setRange(0, processor.irFiles.size() - 1); + irKnob.setRange(0.0, 1.0); + irKnob.setValue(0.0); + irKnob.setSliderStyle(juce::Slider::SliderStyle::RotaryVerticalDrag); + irKnob.setTextBoxStyle(juce::Slider::TextEntryBoxPosition::NoTextBox, false, 50, 20); + irKnob.setNumDecimalPlacesToDisplay(1); + irKnob.setDoubleClickReturnValue(true, 0.0); + + auto irValue = getParameterValue(irName); + Slider& irSlider = getIrSlider(); + irSlider.setValue(irValue, NotificationType::dontSendNotification); + + irKnob.onValueChange = [this] + { + const float sliderValue = static_cast<float> (getIrSlider().getValue()); + const float irValue = getParameterValue(irName); + + if (!approximatelyEqual(irValue, sliderValue)) + { + setParameterValue(irName, sliderValue); + + // create and send an OSC message with an address and a float value: + float value = static_cast<float> (getIrSlider().getValue()); + + if (!oscSender.send(irAddressPattern, value)) + { + updateOutConnectedLabel(false); + } + else + { + DBG("Sent value " + String(value) + " to AP " + irAddressPattern); + } + } + }; + + addAndMakeVisible(irSelect); + irSelect.setColour(juce::Label::textColourId, juce::Colours::black); + int i = 1; + for (const auto& jsonFile : processor.irFiles) { + irSelect.addItem(jsonFile.getFileNameWithoutExtension(), i); + i += 1; + } + irSelect.onChange = [this] {irSelectChanged(); }; + irSelect.setSelectedItemIndex(processor.current_ir_index, juce::NotificationType::dontSendNotification); + irSelect.setScrollWheelEnabled(true); + + addAndMakeVisible(loadIR); + loadIR.setButtonText("Import IR"); + loadIR.setColour(juce::Label::textColourId, juce::Colours::black); + loadIR.addListener(this); + + // Toggle IR + //addAndMakeVisible(irButton); // Toggle is for testing purposes + irButton.setToggleState(true, juce::NotificationType::dontSendNotification); + irButton.onClick = [this] { updateToggleState(&irButton, "IR"); }; + + // Toggle LSTM + //addAndMakeVisible(lstmButton); // Toggle is for testing purposes + lstmButton.setToggleState(true, juce::NotificationType::dontSendNotification); + lstmButton.onClick = [this] { updateToggleState(&lstmButton, "LSTM"); }; + + addAndMakeVisible(ampGainKnob); //ampGainKnob.setLookAndFeel(&ampSilverKnobLAF); ampGainKnob.setTextBoxStyle(juce::Slider::TextEntryBoxPosition::TextBoxBelow, false, 50, 20); @@ -382,14 +449,14 @@ NeuralPiAudioProcessorEditor::NeuralPiAudioProcessorEditor (NeuralPiAudioProcess oscReceiver.getPresenceValue().addListener(this); oscReceiver.getModelValue().addListener(this); + oscReceiver.getIrValue().addListener(this); updateInConnectedLabel(); connectSender(); // Size of plugin GUI - setSize(276, 430); - + setSize(260, 455); } NeuralPiAudioProcessorEditor::~NeuralPiAudioProcessorEditor() @@ -414,50 +481,77 @@ void NeuralPiAudioProcessorEditor::resized() { // This is generally where you'll want to lay out the positions of any // subcomponents in your editor.. - modelSelect.setBounds(19, 10, 234, 25); - loadButton.setBounds(19, 42, 100, 25); + modelSelect.setBounds(11, 10, 234, 25); + loadButton.setBounds(19, 74, 100, 25); modelKnob.setBounds(140, 40, 75, 95); + irSelect.setBounds(11, 42, 234, 25); + loadIR.setBounds(125, 74, 100, 25); + irButton.setBounds(248, 42, 257, 25); + lstmButton.setBounds(248, 10, 257, 25); + // Amp Widgets - ampGainKnob.setBounds(15, 90, 75, 95); - ampMasterKnob.setBounds(100, 90, 75, 95); - ampBassKnob.setBounds(15, 225, 75, 95); - ampMidKnob.setBounds(100, 225, 75, 95); - ampTrebleKnob.setBounds(185, 225, 75, 95); - ampPresenceKnob.setBounds(185, 90, 75, 95); - - GainLabel.setBounds(11, 78, 80, 10); - LevelLabel.setBounds(98, 78, 80, 10); - BassLabel.setBounds(11, 213, 80, 10); - MidLabel.setBounds(97, 213, 80, 10); - TrebleLabel.setBounds(183, 213, 80, 10); - PresenceLabel.setBounds(183, 78, 80, 10); + ampGainKnob.setBounds(10, 120, 75, 95); + ampMasterKnob.setBounds(95, 120, 75, 95); + ampBassKnob.setBounds(10, 250, 75, 95); + ampMidKnob.setBounds(95, 250, 75, 95); + ampTrebleKnob.setBounds(180, 250, 75, 95); + ampPresenceKnob.setBounds(180, 120, 75, 95); + + GainLabel.setBounds(6, 108, 80, 10); + LevelLabel.setBounds(93, 108, 80, 10); + BassLabel.setBounds(6, 238, 80, 10); + MidLabel.setBounds(91, 238, 80, 10); + TrebleLabel.setBounds(178, 238, 80, 10); + PresenceLabel.setBounds(178, 108, 80, 10); addAndMakeVisible(ampNameLabel); ampNameField.setEditable(true, true, true); addAndMakeVisible(ampNameField); // IP controls: - ipField.setBounds(150, 340, 100, 25); - ipLabel.setBounds(15, 340, 150, 25); + ipField.setBounds(150, 365, 100, 25); + ipLabel.setBounds(15, 365, 150, 25); // Port controls: - outPortNumberLabel.setBounds(15, 370, 150, 25); - outPortNumberField.setBounds(160, 370, 75, 25); - inPortNumberLabel.setBounds(15, 400, 150, 25); - inPortNumberField.setBounds(160, 400, 75, 25); + outPortNumberLabel.setBounds(15, 395, 150, 25); + outPortNumberField.setBounds(160, 395, 75, 25); + inPortNumberLabel.setBounds(15, 425, 150, 25); + inPortNumberField.setBounds(160, 425, 75, 25); } void NeuralPiAudioProcessorEditor::modelSelectChanged() { const int selectedFileIndex = modelSelect.getSelectedItemIndex(); + File selectedFile = processor.userAppDataDirectory_tones.getFullPathName() + "/" + modelSelect.getText() + ".json"; if (selectedFileIndex >= 0 && selectedFileIndex < processor.jsonFiles.size()) { - processor.loadConfig(processor.jsonFiles[selectedFileIndex]); - processor.current_model_index = modelSelect.getSelectedItemIndex(); + //processor.loadConfig(processor.jsonFiles[selectedFileIndex]); + processor.loadConfig(selectedFile); + processor.current_model_index = selectedFileIndex; } auto newValue = static_cast<float>(processor.current_model_index / (processor.num_models - 1.0)); modelKnob.setValue(newValue); - //modelKnob.setValue(processor.current_model_index); +} + +void NeuralPiAudioProcessorEditor::irSelectChanged() +{ + const int selectedFileIndex = irSelect.getSelectedItemIndex(); + File selectedFile = processor.userAppDataDirectory_irs.getFullPathName() + "/" + irSelect.getText() + ".wav"; + if (selectedFileIndex >= 0 && selectedFileIndex < processor.irFiles.size()) { + //processor.loadIR(processor.irFiles[selectedFileIndex]); + processor.loadIR(selectedFile); + processor.current_ir_index = selectedFileIndex; + } + auto newValue = static_cast<float>(processor.current_ir_index / (processor.num_irs - 1.0)); + irKnob.setValue(newValue); +} + +void NeuralPiAudioProcessorEditor::updateToggleState(juce::Button* button, juce::String name) +{ + if (name == "IR") + processor.ir_state = button->getToggleState(); + else + processor.lstm_state = button->getToggleState(); } void NeuralPiAudioProcessorEditor::loadButtonClicked() @@ -486,7 +580,7 @@ void NeuralPiAudioProcessorEditor::loadButtonClicked() modelSelect.addItem(file.getFileNameWithoutExtension(), processor.jsonFiles.size() + 1); modelSelect.setSelectedItemIndex(processor.jsonFiles.size(), juce::NotificationType::dontSendNotification); processor.jsonFiles.push_back(file); - //processor.num_models += 1; + processor.num_models += 1; } // Sort jsonFiles alphabetically std::sort(processor.jsonFiles.begin(), processor.jsonFiles.end()); @@ -495,36 +589,66 @@ void NeuralPiAudioProcessorEditor::loadButtonClicked() } } +void NeuralPiAudioProcessorEditor::loadIRClicked() +{ + FileChooser chooser("Select one or more .wav IR files to import", + {}, + "*.wav"); + if (chooser.browseForMultipleFilesToOpen()) + { + int import_fail = 1; + Array<File> files = chooser.getResults(); + for (auto file : files) { + File fullpath = processor.userAppDataDirectory_irs.getFullPathName() + "/" + file.getFileName(); + bool b = fullpath.existsAsFile(); + if (b == false) { + + processor.loadIR(file); + fname = file.getFileName(); + processor.loaded_ir = file; + processor.loaded_ir_name = fname; + processor.custom_ir = 1; + + // Copy selected file to model directory and load into dropdown menu + bool a = file.copyFileTo(fullpath); + if (a == true) { + irSelect.addItem(file.getFileNameWithoutExtension(), processor.irFiles.size() + 1); + irSelect.setSelectedItemIndex(processor.irFiles.size(), juce::NotificationType::dontSendNotification); + processor.irFiles.push_back(file); + processor.num_irs += 1; + } + // Sort jsonFiles alphabetically + std::sort(processor.irFiles.begin(), processor.irFiles.end()); + } + } + } +} + void NeuralPiAudioProcessorEditor::buttonClicked(juce::Button* button) { if (button == &loadButton) { loadButtonClicked(); } + else + { + loadIRClicked(); + } } void NeuralPiAudioProcessorEditor::sliderValueChanged(Slider* slider) { - if (slider == &modelKnob) + if (slider == &modelKnob) { if (slider->getValue() >= 0 && slider->getValue() < processor.jsonFiles.size()) { modelSelect.setSelectedItemIndex(processor.getModelIndex(slider->getValue()), juce::NotificationType::dontSendNotification); } -} -/* - else if (slider == &ampBassKnob || slider == &ampMidKnob || slider == &ampTrebleKnob) { - processor.set_ampEQ(ampBassKnob.getValue(), ampMidKnob.getValue(), ampTrebleKnob.getValue(), ampPresenceKnob.getValue()); - // Set knob states for saving positions when closing/reopening GUI - processor.ampBassKnobState = ampBassKnob.getValue(); - processor.ampMidKnobState = ampMidKnob.getValue(); - processor.ampTrebleKnobState = ampTrebleKnob.getValue(); - } - else if (slider == &ampPresenceKnob) { - processor.set_ampEQ(ampBassKnob.getValue(), ampMidKnob.getValue(), ampTrebleKnob.getValue(), ampPresenceKnob.getValue()); + } else if (slider == &irKnob) { + if (slider->getValue() >= 0 && slider->getValue() < processor.irFiles.size()) { + irSelect.setSelectedItemIndex(processor.getIrIndex(slider->getValue()), juce::NotificationType::dontSendNotification); + } } } -*/ - // OSC Messages Slider& NeuralPiAudioProcessorEditor::getGainSlider() @@ -562,6 +686,11 @@ Slider& NeuralPiAudioProcessorEditor::getModelSlider() return modelKnob; } +Slider& NeuralPiAudioProcessorEditor::getIrSlider() +{ + return irKnob; +} + Label& NeuralPiAudioProcessorEditor::getOutPortNumberField() { @@ -602,6 +731,7 @@ void NeuralPiAudioProcessorEditor::buildAddressPatterns() trebleAddressPattern = "/parameter/" + ampName + "/Treble"; presenceAddressPattern = "/parameter/" + ampName + "/Presence"; modelAddressPattern = "/parameter/" + ampName + "/Model"; + irAddressPattern = "/parameter/" + ampName + "/Ir"; } void NeuralPiAudioProcessorEditor::connectSender() @@ -741,6 +871,14 @@ void NeuralPiAudioProcessorEditor::valueChanged(Value& value) NotificationType::sendNotification); } } + else if (value.refersToSameSourceAs(oscReceiver.getIrValue())) + { + if (!approximatelyEqual(static_cast<double> (value.getValue()), getIrSlider().getValue())) + { + getIrSlider().setValue(static_cast<double> (value.getValue()), + NotificationType::sendNotification); + } + } } void NeuralPiAudioProcessorEditor::timerCallback() @@ -752,6 +890,7 @@ void NeuralPiAudioProcessorEditor::timerCallback() getTrebleSlider().setValue(getParameterValue(trebleName), NotificationType::dontSendNotification); getPresenceSlider().setValue(getParameterValue(presenceName), NotificationType::dontSendNotification); getModelSlider().setValue(getParameterValue(modelName), NotificationType::dontSendNotification); + getIrSlider().setValue(getParameterValue(irName), NotificationType::dontSendNotification); } AudioProcessorParameter* NeuralPiAudioProcessorEditor::getParameter(const String& paramId) diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h @@ -46,6 +46,7 @@ public: String gainAddressPattern{ "/parameter/NeuralPi/Gain" }; String masterAddressPattern{ "/parameter/NeuralPi/Master" }; String modelAddressPattern{ "/parameter/NeuralPi/Model" }; + String irAddressPattern{ "/parameter/NeuralPi/Ir" }; String bassAddressPattern{ "/parameter/NeuralPi/Bass" }; String midAddressPattern{ "/parameter/NeuralPi/Mid" }; String trebleAddressPattern{ "/parameter/NeuralPi/Treble" }; @@ -59,7 +60,7 @@ public: const String presenceName{ "presence" }; const String modelName{ "model" }; - + const String irName{ "ir" }; private: // This reference is provided as a quick way for your editor to @@ -72,9 +73,11 @@ private: Slider ampGainKnob; Slider ampMasterKnob; Slider modelKnob; + Slider irKnob; //ImageButton ampOnButton; //ImageButton ampLED; ComboBox modelSelect; + ComboBox irSelect; Slider ampBassKnob; Slider ampMidKnob; Slider ampTrebleKnob; @@ -91,10 +94,17 @@ private: File model_folder; TextButton loadButton; + TextButton loadIR; + ToggleButton irButton; + ToggleButton lstmButton; + juce::String fname; virtual void buttonClicked(Button* button) override; void modelSelectChanged(); void loadButtonClicked(); + void updateToggleState(juce::Button* button, juce::String name); + void irSelectChanged(); + void loadIRClicked(); virtual void sliderValueChanged(Slider* slider) override; @@ -122,6 +132,7 @@ private: Slider& getGainSlider(); Slider& getMasterSlider(); Slider& getModelSlider(); + Slider& getIrSlider(); Slider& getBassSlider(); Slider& getMidSlider(); Slider& getTrebleSlider(); diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp @@ -30,12 +30,18 @@ NeuralPiAudioProcessor::NeuralPiAudioProcessor() setupDataDirectories(); installTones(); resetDirectory(userAppDataDirectory_tones); + // Sort jsonFiles alphabetically + std::sort(jsonFiles.begin(), jsonFiles.end()); if (jsonFiles.size() > 0) { loadConfig(jsonFiles[current_model_index]); } - // Sort jsonFiles alphabetically - std::sort(jsonFiles.begin(), jsonFiles.end()); + resetDirectoryIR(userAppDataDirectory_irs); + // Sort irFiles alphabetically + std::sort(irFiles.begin(), irFiles.end()); + if (irFiles.size() > 0) { + loadIR(irFiles[current_ir_index]); + } // initialize parameters: addParameter(gainParam = new AudioParameterFloat(GAIN_ID, GAIN_NAME, NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.5f)); @@ -45,6 +51,7 @@ NeuralPiAudioProcessor::NeuralPiAudioProcessor() addParameter(trebleParam = new AudioParameterFloat(TREBLE_ID, TREBLE_NAME, NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.5f)); addParameter(presenceParam = new AudioParameterFloat(PRESENCE_ID, PRESENCE_NAME, NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.5f)); addParameter(modelParam = new AudioParameterFloat(MODEL_ID, MODEL_NAME, NormalisableRange<float>(0.0f, 1.0f, 0.001f), 0.0f)); + addParameter(irParam = new AudioParameterFloat(IR_ID, IR_NAME, NormalisableRange<float>(0.0f, 1.0f, 0.001f), 0.0f)); } @@ -125,6 +132,9 @@ void NeuralPiAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlo dcBlocker.coefficients = dsp::IIR::Coefficients<float>::makeHighPass(sampleRate, 35.0f); dsp::ProcessSpec spec{ sampleRate, static_cast<uint32> (samplesPerBlock), 2 }; dcBlocker.prepare(spec); + + // Set up IR + cabSimIR.prepare(spec); } void NeuralPiAudioProcessor::releaseResources() @@ -171,21 +181,24 @@ void NeuralPiAudioProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffe if (amp_state == 1) { auto gain = static_cast<float> (gainParam->get()); auto master = static_cast<float> (masterParam->get()); - // Note: Default 0.0 -> 1.0 param range is converted to +-8.0 here - auto bass = (static_cast<float> (bassParam->get() - 0.5) * 16.0); - auto mid = (static_cast<float> (midParam->get() - 0.5) * 16.0); - auto treble = (static_cast<float> (trebleParam->get() - 0.5) * 16.0); - auto presence = (static_cast<float> (presenceParam->get() - 0.5) * 16.0); + // Note: Default 0.0 -> 1.0 param range is converted to +-12.0 here + auto bass = (static_cast<float> (bassParam->get() - 0.5) * 24.0); + auto mid = (static_cast<float> (midParam->get() - 0.5) * 24.0); + auto treble = (static_cast<float> (trebleParam->get() - 0.5) * 24.0); + auto presence = (static_cast<float> (presenceParam->get() - 0.5) * 24.0); auto model = static_cast<float> (modelParam->get()); model_index = getModelIndex(model); + auto ir = static_cast<float> (irParam->get()); + ir_index = getIrIndex(ir); + buffer.applyGain(gain * 2.0); eq4band.setParameters(bass, mid, treble, presence);// Better to move this somewhere else? Only need to set when value changes eq4band.process(buffer.getReadPointer(0), buffer.getWritePointer(0), midiMessages, numSamples, numInputChannels, sampleRate); // Apply LSTM model - if (model_loaded == 1) { + if (model_loaded == 1 && lstm_state == true) { if (current_model_index != model_index) { loadConfig(jsonFiles[model_index]); current_model_index = model_index; @@ -193,6 +206,20 @@ void NeuralPiAudioProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffe LSTM.process(buffer.getReadPointer(0), buffer.getWritePointer(0), numSamples); } + // Process IR + if (ir_state == true && num_irs > 0) { + if (current_ir_index != ir_index) { + loadIR(irFiles[ir_index]); + current_ir_index = ir_index; + } + auto block = dsp::AudioBlock<float>(buffer).getSingleChannelBlock(0); + auto context = juce::dsp::ProcessContextReplacing<float>(block); + cabSimIR.process(context); + + // IR generally makes output quieter, add volume here to make ir on/off volume more even + buffer.applyGain(2.0); + } + // Master Volume buffer.applyGain(master); } @@ -228,6 +255,7 @@ void NeuralPiAudioProcessor::getStateInformation(MemoryBlock& destData) stream.writeFloat(*trebleParam); stream.writeFloat(*presenceParam); stream.writeFloat(*modelParam); + stream.writeFloat(*irParam); } void NeuralPiAudioProcessor::setStateInformation(const void* data, int sizeInBytes) @@ -241,12 +269,11 @@ void NeuralPiAudioProcessor::setStateInformation(const void* data, int sizeInByt trebleParam->setValueNotifyingHost(stream.readFloat()); presenceParam->setValueNotifyingHost(stream.readFloat()); modelParam->setValueNotifyingHost(stream.readFloat()); + irParam->setValueNotifyingHost(stream.readFloat()); } int NeuralPiAudioProcessor::getModelIndex(float model_param) { - //return static_cast<int>(model_param * (jsonFiles.size() - 1.0)); - //return static_cast<int>(model_param * (num_models - 1.0)); int a = static_cast<int>(round(model_param * (num_models - 1.0))); if (a > num_models - 1) { a = num_models - 1; @@ -257,16 +284,48 @@ int NeuralPiAudioProcessor::getModelIndex(float model_param) return a; } +int NeuralPiAudioProcessor::getIrIndex(float ir_param) +{ + int a = static_cast<int>(round(ir_param * (num_irs - 1.0))); + if (a > num_irs - 1) { + a = num_irs - 1; + } + else if (a < 0) { + a = 0; + } + return a; +} + void NeuralPiAudioProcessor::loadConfig(File configFile) { this->suspendProcessing(true); - model_loaded = 1; String path = configFile.getFullPathName(); char_filename = path.toUTF8(); - // TODO Add check here for invalid files - LSTM.load_json(char_filename); + try { + LSTM.load_json(char_filename); + model_loaded = 1; + } + catch (const std::exception& e) { + DBG("Unable to load IR file: " + configFile.getFullPathName()); + std::cout << e.what(); + } + + this->suspendProcessing(false); +} + +void NeuralPiAudioProcessor::loadIR(File irFile) +{ + this->suspendProcessing(true); + try { + cabSimIR.load(irFile); + ir_loaded = 1; + } + catch (const std::exception& e) { + DBG("Unable to load IR file: " + irFile.getFullPathName()); + std::cout << e.what(); + } this->suspendProcessing(false); } @@ -282,6 +341,19 @@ void NeuralPiAudioProcessor::resetDirectory(const File& file) } } +void NeuralPiAudioProcessor::resetDirectoryIR(const File& file) +{ + irFiles.clear(); + if (file.isDirectory()) + { + juce::Array<juce::File> results; + file.findChildFiles(results, juce::File::findFiles, false, "*.wav"); + for (int i = results.size(); --i >= 0;) + irFiles.push_back(File(results.getReference(i).getFullPathName())); + + } +} + void NeuralPiAudioProcessor::addDirectory(const File& file) { if (file.isDirectory()) @@ -296,6 +368,20 @@ void NeuralPiAudioProcessor::addDirectory(const File& file) } } +void NeuralPiAudioProcessor::addDirectoryIR(const File& file) +{ + if (file.isDirectory()) + { + juce::Array<juce::File> results; + file.findChildFiles(results, juce::File::findFiles, false, "*.wav"); + for (int i = results.size(); --i >= 0;) + { + irFiles.push_back(File(results.getReference(i).getFullPathName())); + num_irs = num_irs + 1.0; + } + } +} + void NeuralPiAudioProcessor::setupDataDirectories() { // User app data directory @@ -303,6 +389,7 @@ void NeuralPiAudioProcessor::setupDataDirectories() File userAppDataTempFile_tones = userAppDataDirectory_tones.getChildFile("tmp.pdl"); + File userAppDataTempFile_irs = userAppDataDirectory_irs.getChildFile("tmp.pdl"); // Create (and delete) temp file if necessary, so that user doesn't have // to manually create directories @@ -320,9 +407,17 @@ void NeuralPiAudioProcessor::setupDataDirectories() userAppDataTempFile_tones.deleteFile(); } + if (!userAppDataDirectory_irs.exists()) { + userAppDataTempFile_irs.create(); + } + if (userAppDataTempFile_irs.existsAsFile()) { + userAppDataTempFile_irs.deleteFile(); + } + // Add the tones directory and update tone list addDirectory(userAppDataDirectory_tones); + addDirectoryIR(userAppDataDirectory_irs); } void NeuralPiAudioProcessor::installTones() diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h @@ -12,6 +12,7 @@ #include "RTNeuralLSTM.h" #include "AmpOSCReceiver.h" #include "Eq4Band.h" +#include "CabSim.h" #pragma once @@ -21,6 +22,8 @@ #define GAIN_NAME "Gain" #define MODEL_ID "model" #define MODEL_NAME "Model" +#define IR_ID "ir" +#define IR_NAME "Ir" #define MASTER_ID "master" #define MASTER_NAME "Master" #define BASS_ID "bass" @@ -75,31 +78,29 @@ public: void getStateInformation (MemoryBlock& destData) override; void setStateInformation (const void* data, int sizeInBytes) override; - bool compareFunction(juce::File a, juce::File b); int getModelIndex(float model_param); + int getIrIndex(float ir_param); void loadConfig(File configFile); + void loadIR(File irFile); void setupDataDirectories(); void installTones(); void set_ampEQ(float bass_slider, float mid_slider, float treble_slider, float presence_slider); - // Overdrive Pedal float convertLogScale(float in_value, float x_min, float x_max, float y_min, float y_max); - // Amp - /* - void set_ampDrive(float db_ampCleanDrive); - void set_ampMaster(float db_ampMaster); - void set_ampEQ(float bass_slider, float mid_slider, float treble_slider, float presence_slider); - */ float decibelToLinear(float dbValue); void addDirectory(const File& file); + void addDirectoryIR(const File& file); void resetDirectory(const File& file); + void resetDirectoryIR(const File& file); std::vector<File> jsonFiles; + std::vector<File> irFiles; File currentDirectory = File::getCurrentWorkingDirectory().getFullPathName(); File userAppDataDirectory = File::getSpecialLocation(File::userDocumentsDirectory).getChildFile(JucePlugin_Manufacturer).getChildFile(JucePlugin_Name); File userAppDataDirectory_tones = userAppDataDirectory.getFullPathName() + "/tones"; + File userAppDataDirectory_irs = userAppDataDirectory.getFullPathName() + "/irs"; // Pedal/amp states int amp_state = 1; // 0 = off, 1 = on @@ -111,6 +112,16 @@ public: int current_model_index = 0; float num_models = 0.0; int model_index = 0; // Used in processBlock when converting slider param to model index + bool lstm_state = true; + + juce::String loaded_ir_name; + float num_irs = 0.0; + int ir_loaded = 0; + int custom_ir = 0; // 0 = custom tone loaded, 1 = default channel tone + File loaded_ir; + bool ir_state = true; + int current_ir_index = 0; + int ir_index = 0; RT_LSTM LSTM; @@ -125,9 +136,13 @@ private: AudioParameterFloat* trebleParam; AudioParameterFloat* presenceParam; AudioParameterFloat* modelParam; + AudioParameterFloat* irParam; dsp::IIR::Filter<float> dcBlocker; + // IR processing + CabSim cabSimIR; + //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NeuralPiAudioProcessor) }; diff --git a/resources/cassette_recorder.wav b/resources/cassette_recorder.wav Binary files differ. diff --git a/resources/guitar_amp.wav b/resources/guitar_amp.wav Binary files differ. diff --git a/resources/npi_background.jpg b/resources/npi_background.jpg Binary files differ. diff --git a/scripts/update_models.bat b/scripts/update_models.bat @@ -1,10 +1,10 @@ ::############################################################################ :: NeuralPi - Update Models - Windows Script :: -:: This script transfers models from a Windows computer to the NeuralPi, -:: and from the NeuralPi back to the host computer. Edit the Raspberry Pi -:: IP address (after connecting to a local Wifi Network), and run this -:: script from a Windows computer running the NeuralPi plugin. +:: This script transfers models and impulse responses from a Windows computer +:: to the NeuralPi, and from the NeuralPi back to the host computer. Edit +:: the Raspberry Pi IP address (after connecting to a local Wifi Network), +:: and run this script from a Windows computer running the NeuralPi plugin. :: :: Note: Ensure OpenSSH is installed. This comes installed with Windows as of 2018. ::############################################################################ @@ -15,17 +15,26 @@ set "rpi_ip_address=127.0.0.1" -:: Typical Windows 10 Path, edit <YOUR_USERNAME> with your Windows Username -set "host_model_path=C:/Users/<YOUR_USERNAME>/Documents/GuitarML/NeuralPi/tones" +:: Typical Windows 10 Path (shouldn't need to change) +set "host_model_path=%userprofile%/Documents/GuitarML/NeuralPi/tones" +set "host_ir_path=%userprofile%/Documents/GuitarML/NeuralPi/irs" :: Rpi with Elk OS Path (shouldn't need to change) set "rpi_model_path=/home/mind/Documents/GuitarML/NeuralPi/tones" +set "rpi_ir_path=/home/mind/Documents/GuitarML/NeuralPi/irs" :: ############################################################################ :: Copy all models from local computer to Rpi scp %host_model_path%/*.json root@%rpi_ip_address%:%rpi_model_path%/ +:: Copy all IRs from local computer to Rpi +scp %host_ir_path%/*.wav root@%rpi_ip_address%:%rpi_ir_path%/ + + :: Copy all models from Rpi to local computer scp root@%rpi_ip_address%:%rpi_model_path%/*.json %host_model_path%/ + +:: Copy all IRs from Rpi to local computer +scp root@%rpi_ip_address%:%rpi_ir_path%/*.wav %host_ir_path%/ diff --git a/scripts/update_models.sh b/scripts/update_models.sh @@ -18,13 +18,22 @@ rpi_ip_address=127.0.0.1 # Update this field with the Raspberry Pi's IP address # Uncomment the appropriate path for your computer: host_model_path=~/Documents/GuitarML/NeuralPi/tones #Typical Mac/Linux Path (shouldn't need to change) +host_ir_path=~/Documents/GuitarML/NeuralPi/irs #Typical Mac/Linux Path (shouldn't need to change) rpi_model_path=/home/mind/Documents/GuitarML/NeuralPi/tones # Rpi with Elk OS Path (shouldn't need to change) +rpi_ir_path=/home/mind/Documents/GuitarML/NeuralPi/irs # Rpi with Elk OS Path (shouldn't need to change) ############################################################################# echo "Copying all models from local computer to Rpi.." scp $host_model_path/*.json root@$rpi_ip_address:$rpi_model_path/ +echo "Copying all IRs from local computer to Rpi.." +scp $host_ir_path/*.json root@$rpi_ip_address:$rpi_ir_path/ + + echo "Copying all models from Rpi to local computer.." -scp root@$rpi_ip_address:$rpi_model_path/*.json $host_model_path/ +scp root@$rpi_ip_address:$rpi_model_path/*.wav $host_model_path/ + +echo "Copying all IRs from Rpi to local computer.." +scp root@$rpi_ip_address:$rpi_ir_path/*.wav $host_ir_path/