Skip to content
Open
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
9 changes: 6 additions & 3 deletions .github/workflows/macos-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ jobs:
echo "- \`git archive\` SHA-256: \`$source_hash\`"
} >> "$GITHUB_STEP_SUMMARY"

- name: Versioned state-layout migration tests
run: bash NeuralAmpModeler/tests/run_unserialization_layout_tests.sh
- name: Versioned state-layout and transpose DSP tests
run: |
bash NeuralAmpModeler/tests/run_unserialization_layout_tests.sh
bash NeuralAmpModeler/tests/run_transpose_processor_tests.sh

- name: Python trainer security regression tests
run: |
Expand Down Expand Up @@ -89,6 +91,7 @@ jobs:
AudioDSPTools/tests/run_wav_tests.sh \
AudioDSPTools/tests/run_au_offline_test.sh \
NeuralAmpModeler/tests/run_unserialization_layout_tests.sh \
NeuralAmpModeler/tests/run_transpose_processor_tests.sh \
NeuralAmpModeler/tests/run_au_state_migration_test.sh
plutil -lint NeuralAmpModeler/resources/*.plist NeuralAmpModeler/resources/DessMetal-macOS-release.entitlements
version="$DESSMETAL_VERSION"
Expand Down Expand Up @@ -174,7 +177,7 @@ jobs:
mkdir -p "$dmg_mount"
hdiutil attach -readonly -nobrowse -mountpoint "$dmg_mount" "$dmg"
trap 'hdiutil detach "$dmg_mount" >/dev/null 2>&1 || true' EXIT
for license in RtAudio-MIT.txt RtMidi-MIT.txt nlohmann-json-MIT.txt stb-MIT.txt; do
for license in RtAudio-MIT.txt RtMidi-MIT.txt nlohmann-json-MIT.txt stb-MIT.txt terrarium-poly-octave-MIT.txt; do
test -f "$dmg_mount/THIRD_PARTY_LICENSES/$license"
done
hdiutil detach "$dmg_mount"
Expand Down
95 changes: 93 additions & 2 deletions AudioDSPTools/tests/test_au_offline.mm
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
constexpr AudioUnitParameterID kDriveModelParameter = 14;
constexpr AudioUnitParameterID kAmpModelParameter = 18;
constexpr AudioUnitParameterID kAmpActiveParameter = 19;
constexpr AudioUnitParameterID kTransposeParameter = 20;

struct InputState
{
Expand Down Expand Up @@ -64,6 +65,23 @@ AudioStreamBasicDescription MakeFormat(const UInt32 channels)
format.mBitsPerChannel = 8 * sizeof(float);
return format;
}

double ToneMagnitude(const std::vector<float>& signal, const double frequency)
{
double real = 0.0;
double imaginary = 0.0;
double windowSum = 0.0;
for (std::size_t index = 0; index < signal.size(); ++index)
{
const double window = 0.5 - 0.5 * std::cos(2.0 * M_PI * index / (signal.size() - 1));
const double angle = 2.0 * M_PI * frequency * index / kSampleRate;
const double sample = signal[index] * window;
real += sample * std::cos(angle);
imaginary -= sample * std::sin(angle);
windowSum += window;
}
return 2.0 * std::hypot(real, imaginary) / windowSum;
}
} // namespace

int main()
Expand Down Expand Up @@ -137,7 +155,8 @@ int main()
bool finite = true;
UInt32 renderedBlocks = 0;
const auto renderBlocks = [&](const UInt32 blockCount, double& measuredPeak, double* maxAdjacentStep = nullptr,
double* previousLeftSample = nullptr) {
double* previousLeftSample = nullptr,
std::vector<float>* capturedLeft = nullptr) {
measuredPeak = 0.0;
if (maxAdjacentStep != nullptr)
*maxAdjacentStep = 0.0;
Expand All @@ -153,6 +172,8 @@ int main()
AudioUnitRenderActionFlags flags = 0;
if (!Check(AudioUnitRender(unit, &flags, &timestamp, 0, kFramesPerBlock, &output.list), "AudioUnitRender"))
return false;
if (capturedLeft != nullptr)
capturedLeft->insert(capturedLeft->end(), left.begin(), left.end());
for (const float sample : left)
{
finite = finite && std::isfinite(sample);
Expand Down Expand Up @@ -274,6 +295,74 @@ int main()
return 1;
}

// Exercise the exact installed AU's new processing path with the nonlinear
// stages bypassed. The target carrier must dominate nearby output at both
// octave extremes, automation must remain bounded, and host PDC must not
// change with the transpose value.
std::vector<double> transposeCarrierDeficits;
double transposeMaxStep = 0.0;
if (!Check(AudioUnitSetParameter(unit, kDriveActiveParameter, kAudioUnitScope_Global, 0, 0.0f, 0),
"disable drive for transpose test"))
{
dispose();
return 1;
}
for (const int semitones : {-12, 12})
{
double transitionPeak = 0.0;
double transitionStep = 0.0;
std::vector<float> captured;
captured.reserve(64 * kFramesPerBlock);
if (!Check(AudioUnitSetParameter(unit, kTransposeParameter, kAudioUnitScope_Global, 0,
static_cast<AudioUnitParameterValue>(semitones), 0),
"set transpose")
|| !renderBlocks(16, transitionPeak, &transitionStep, &bypassPreviousSample)
|| !renderBlocks(64, transitionPeak, nullptr, nullptr, &captured))
{
dispose();
return 1;
}
transposeMaxStep = std::max(transposeMaxStep, transitionStep);
const double expectedHz = 220.0 * std::pow(2.0, semitones / 12.0);
const double targetMagnitude = ToneMagnitude(captured, expectedHz);
double strongestMagnitude = 0.0;
for (double candidate = expectedHz * 0.85; candidate <= expectedHz * 1.15; candidate += 0.25)
strongestMagnitude = std::max(strongestMagnitude, ToneMagnitude(captured, candidate));
const double carrierDeficit = 20.0 * std::log10(std::max(strongestMagnitude, 1.0e-15)
/ std::max(targetMagnitude, 1.0e-15));
transposeCarrierDeficits.push_back(carrierDeficit);
if (targetMagnitude < 1.0e-5 || carrierDeficit > 3.0)
{
std::cerr << "Transpose " << semitones << " target carrier failed: magnitude=" << targetMagnitude
<< " deficit=" << carrierDeficit << " dB\n";
dispose();
return 1;
}
}

Float64 latencyAfterTransposeSeconds = 0.0;
UInt32 latencyAfterTransposeSize = sizeof(latencyAfterTransposeSeconds);
if (!Check(AudioUnitGetProperty(unit, kAudioUnitProperty_Latency, kAudioUnitScope_Global, 0,
&latencyAfterTransposeSeconds, &latencyAfterTransposeSize),
"get latency after transpose")
|| std::abs(latencyAfterTransposeSeconds - latencySeconds) > (0.5 / kSampleRate)
|| transposeMaxStep > 0.25)
{
std::cerr << "Transpose automation changed latency or continuity: latency="
<< latencyAfterTransposeSeconds * kSampleRate << " samples, max-step=" << transposeMaxStep << '\n';
dispose();
return 1;
}

double transposeBypassPeak = 0.0;
if (!Check(AudioUnitSetParameter(unit, kTransposeParameter, kAudioUnitScope_Global, 0, 0.0f, 0),
"reset transpose")
|| !renderBlocks(8, transposeBypassPeak))
{
dispose();
return 1;
}

Float64 latencyDuringBypassSelectionSeconds = 0.0;
UInt32 latencyDuringBypassSelectionSize = sizeof(latencyDuringBypassSelectionSeconds);
if (!Check(AudioUnitGetProperty(unit, kAudioUnitProperty_Latency, kAudioUnitScope_Global, 0,
Expand Down Expand Up @@ -339,6 +428,8 @@ int main()
<< ", SickDess=" << sickDessPeak << ", drive models=" << drivePeaks[0] << "/" << drivePeaks[1]
<< "/" << drivePeaks[2] << "/" << drivePeaks[3]
<< ", bypass model-switch max-step=" << modelSwitchBypassMaxStep << ", latency="
<< latencySeconds * kSampleRate << " samples, elapsed=" << elapsed << "s\n";
<< latencySeconds * kSampleRate << " samples, transpose deficits="
<< transposeCarrierDeficits[0] << "/" << transposeCarrierDeficits[1]
<< " dB, transpose max-step=" << transposeMaxStep << ", elapsed=" << elapsed << "s\n";
return 0;
}
57 changes: 52 additions & 5 deletions NeuralAmpModeler/NeuralAmpModeler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,21 @@ NeuralAmpModeler::NeuralAmpModeler(const InstanceInfo& info)
GetParam(kBoostModel)->InitEnum("Boost Model", 0, {"OD808", "SD1", "TS9", "aesahaettr"});

GetParam(kNAMActive)->InitBool("Amp Enabled", true); // Default Amp On
GetParam(kTransposeSemitones)->InitInt("Transpose", 0, -12, 12, "st");
GetParam(kTransposeSemitones)->SetDisplayFunc([](const double value, WDL_String& display) {
const int semitones = static_cast<int>(std::lround(value));
if (semitones > 0)
display.SetFormatted(16, "+%d", semitones);
else
display.SetFormatted(16, "%d", semitones);
});

mAmpModelIdx.store(GetParam(kAmpModel)->Int(), std::memory_order_relaxed);
mAmpActiveTarget.store(GetParam(kNAMActive)->Bool(), std::memory_order_relaxed);
mBoostModelIdx.store(GetParam(kBoostModel)->Int(), std::memory_order_relaxed);
mBoostActiveTarget.store(GetParam(kBoostActive)->Bool(), std::memory_order_relaxed);
mIRActiveTarget.store(GetParam(kIRToggle)->Bool(), std::memory_order_relaxed);
mTransposeSemitones.store(GetParam(kTransposeSemitones)->Int(), std::memory_order_relaxed);

mNoiseGateTrigger.AddListener(&mNoiseGateGain);
mCurrentParams.resize(1, 0.5); // Init params
Expand Down Expand Up @@ -301,6 +310,7 @@ NeuralAmpModeler::NeuralAmpModeler(const InstanceInfo& info)
// the amp controls. It avoids squeezing long model names into a small menu.
const auto ampModelLabelArea = IRECT(370, 132, 455, 164);
const auto ampModelSwitchArea = IRECT(455, 132, 910, 164);
const auto transposeArea = IRECT(74, 110, 336, 178);

// Text-labelled effect buttons share one clear row on the lower amp panel.
const auto ampBypassArea = IRECT(400, 294, 530, 359);
Expand Down Expand Up @@ -373,6 +383,26 @@ NeuralAmpModeler::NeuralAmpModeler(const InstanceInfo& info)
const auto driveMenuStyle =
actionButtonStyle.WithValueText(IText(14.f, EAlign::Center, COLOR_WHITE));
const auto pedalKnobStyle = style.WithShowLabel(false).WithShowValue(false);
const auto transposeStyle =
style.WithShowLabel(true)
.WithShowValue(true)
.WithDrawFrame(true)
.WithDrawShadows(false)
.WithEmboss(false)
.WithRoundness(0.18f)
.WithFrameThickness(1.f)
.WithWidgetFrac(0.70f)
.WithColor(kBG, COLOR_BLACK.WithOpacity(0.72f))
.WithColor(kFG, PluginColors::NAM_THEMECOLOR.WithOpacity(0.82f))
.WithColor(kFR, COLOR_WHITE.WithOpacity(0.28f))
.WithColor(kHL, COLOR_WHITE.WithOpacity(0.12f))
.WithLabelText(IText(13.f, EAlign::Center, PluginColors::NAM_THEMEFONTCOLOR))
.WithValueText(IText(17.f, EAlign::Center, COLOR_WHITE));

auto* transposeControl =
pGraphics->AttachControl(new NAMTransposeControl(transposeArea, kTransposeSemitones, transposeStyle));
transposeControl->SetTooltip(
"Retune the guitar from -12 to +12 semitones. Double-click to return to standard pitch.");

auto* irSwitchControl = pGraphics->AttachControl(
new IVToggleControl(irSwitchArea, kIRToggle, "", actionButtonStyle, "CAB IR OFF", "CAB IR ON"));
Expand Down Expand Up @@ -614,6 +644,11 @@ void NeuralAmpModeler::ProcessBlock(iplug::sample** inputs, iplug::sample** outp
}
}

// Retune the calibrated mono guitar before every nonlinear stage. The
// filter bank has no buffered look-ahead, so this does not change host PDC.
mTransposeProcessor.ProcessInPlace(
mInputPointers[0], nFrames, mTransposeSemitones.load(std::memory_order_acquire));

const bool noiseGateActive = GetParam(kNoiseGateActive)->Value();
const bool toneStackActive = GetParam(kEQActive)->Value();

Expand Down Expand Up @@ -764,6 +799,8 @@ void NeuralAmpModeler::OnReset()
mNoiseGateTrigger.PrepareBuffers(kNumChannelsInternal, maxBlockSize);
mNoiseGateGain.PrepareBuffers(kNumChannelsInternal, maxBlockSize);
mHighPass.PrepareBuffers(kNumChannelsInternal, maxBlockSize);
mTransposeSemitones.store(GetParam(kTransposeSemitones)->Int(), std::memory_order_release);
mTransposeProcessor.Reset(sampleRate, mTransposeSemitones.load(std::memory_order_acquire));

const double bass = GetParam(kToneBass)->Value();
const double middle = GetParam(kToneMid)->Value();
Expand Down Expand Up @@ -1098,12 +1135,17 @@ void NeuralAmpModeler::OnUIOpen()
}

const WDL_String boostPath = _GetBoostNAMPathSnapshot();
if (boostPath.GetLength())
// The fixed drive selector replaced the historical Boost file browser. Old
// standalone preferences can still contain a path, so never message the
// removed control during editor attachment.
if (boostPath.GetLength() && GetUI() != nullptr
&& GetUI()->GetControlWithTag(kCtrlTagBoostModelFileBrowser) != nullptr)
{
SendControlMsgFromDelegate(kCtrlTagBoostModelFileBrowser, kMsgTagLoadedBoostModel, boostPath.GetLength(), boostPath.Get());
if (mBoostModel.load(std::memory_order_acquire) == nullptr
&& mPendingBoostModel.load(std::memory_order_acquire) == nullptr)
SendControlMsgFromDelegate(kCtrlTagBoostModelFileBrowser, kMsgTagLoadFailed);
SendControlMsgFromDelegate(kCtrlTagBoostModelFileBrowser, kMsgTagLoadedBoostModel, boostPath.GetLength(),
boostPath.Get());
if (mBoostModel.load(std::memory_order_acquire) == nullptr
&& mPendingBoostModel.load(std::memory_order_acquire) == nullptr)
SendControlMsgFromDelegate(kCtrlTagBoostModelFileBrowser, kMsgTagLoadFailed);
}
}

Expand Down Expand Up @@ -1165,6 +1207,9 @@ void NeuralAmpModeler::OnParamChange(int paramIdx)
mIRLoadRequest.fetch_add(1, std::memory_order_release);
break;
}
case kTransposeSemitones:
mTransposeSemitones.store(GetParam(kTransposeSemitones)->Int(), std::memory_order_release);
break;

// Changes to the input gain
case kCalibrateInput:
Expand Down Expand Up @@ -1961,6 +2006,8 @@ void NeuralAmpModeler::_UpdateLatency()
&& mModelMetadataEpoch.load(std::memory_order_relaxed) == mAudioConfigEpoch.load(std::memory_order_acquire))
latency += mModelMetadataLatency.load(std::memory_order_relaxed);
// Other things that add latency here...
// Transpose uses causal analytic filters without a buffered look-ahead, so
// its host-reported latency is intentionally zero at every semitone value.

// Feels weird to have to do this.
if (GetLatency() != latency)
Expand Down
5 changes: 5 additions & 0 deletions NeuralAmpModeler/NeuralAmpModeler.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#include "Colors.h"
#include "ToneStack.h"
#include "TransposeProcessor.h"

#include "IPlug_include_in_plug_hdr.h"
#include "ISender.h"
Expand Down Expand Up @@ -57,6 +58,8 @@ enum EParams
// restores the owner-authored SickDess capture.
kAmpModel,
kNAMActive, // Bypass NAM Processing
// Append new host parameters after the complete 0.1 layout.
kTransposeSemitones,
kNumParams
};

Expand Down Expand Up @@ -418,6 +421,7 @@ class NeuralAmpModeler final : public iplug::Plugin
// Noise gates
dsp::noise_gate::Trigger mNoiseGateTrigger;
dsp::noise_gate::Gain mNoiseGateGain;
dessmetal::transpose::Processor mTransposeProcessor;
// Live DSP pointers are owned by the plug-in, but are intentionally raw:
// ProcessBlock may swap them without running a destructor. Producers publish
// complete objects through the atomic pending slots; the serialized loader
Expand Down Expand Up @@ -486,6 +490,7 @@ class NeuralAmpModeler final : public iplug::Plugin
std::atomic<int> mBoostModelIdx{0};
std::atomic<bool> mBoostActiveTarget{false};
std::atomic<bool> mIRActiveTarget{true};
std::atomic<int> mTransposeSemitones{0};
std::atomic<double> mTargetGain{0.5};
std::atomic<double> mAudioSampleRate{48000.0};
std::atomic<int> mAudioMaxBlockSize{2048};
Expand Down
64 changes: 64 additions & 0 deletions NeuralAmpModeler/NeuralAmpModelerControls.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <sstream> // std::stringstream
#include <unordered_map> // std::unordered_map
#include "IControls.h"
#include "IVNumberBoxControl.h"

#define PLUG() static_cast<PLUG_CLASS_NAME*>(GetDelegate())
#define NAM_KNOB_HEIGHT 120.0f
Expand Down Expand Up @@ -103,6 +104,69 @@ class NAMStompToggleControl : public IVToggleControl
}
};

class NAMTransposeControl : public IVNumberBoxControl
{
public:
NAMTransposeControl(const IRECT& bounds, const int paramIdx, const IVStyle& style)
: IVNumberBoxControl(bounds, paramIdx, nullptr, "TRANSPOSE", style, true, 0.0, -12.0, 12.0,
"%+.0f", false)
{
}

void OnAttached() override
{
IVNumberBoxControl::OnAttached();

// The stock number-box stacks two half-height buttons on the right. That
// works for mouse-heavy desktop utilities, but it is unnecessarily fiddly
// for a control guitarists may change mid-session. Keep the parameter and
// host-notification behavior from IVNumberBoxControl while giving both
// directions a full-height, 54 px target and a more legible glyph.
const auto buttonStyle =
mStyle.WithShowLabel(true)
.WithShowValue(false)
.WithWidgetFrac(1.f)
.WithLabelText(IText(25.f, EAlign::Center, COLOR_WHITE))
.WithValueText(IText(25.f, EAlign::Center, COLOR_WHITE));
mDecButton->SetStyle(buttonStyle);
mIncButton->SetStyle(buttonStyle);
OnResize();
}

void OnResize() override
{
MakeRects(mRECT, false);

auto sections = mWidgetBounds.GetPadded(-1.f);
constexpr float buttonWidth = 54.f;
constexpr float controlGap = 4.f;
const auto decBounds = sections.GetFromLeft(buttonWidth);
const auto incBounds = sections.GetFromRight(buttonWidth);
const auto valueBounds = IRECT(decBounds.R + controlGap, sections.T,
incBounds.L - controlGap, sections.B);

if (mTextReadout)
mTextReadout->SetTargetAndDrawRECTs(valueBounds);
if (mDecButton)
mDecButton->SetTargetAndDrawRECTs(decBounds);
if (mIncButton)
mIncButton->SetTargetAndDrawRECTs(incBounds);

SetTargetRECT(valueBounds);
SetDirty(false);
}

void OnMouseDblClick(float, float, const IMouseMod&) override
{
if (IsDisabled() || GetParam() == nullptr)
return;
GetDelegate()->BeginInformHostOfParamChangeFromUI(GetParamIdx());
mRealValue = 0.0;
OnValueChanged();
GetDelegate()->EndInformHostOfParamChangeFromUI(GetParamIdx());
}
};

// Scaled bitmap background control - draws bitmap scaled to fill the entire bounds
class NAMScaledBitmapControl : public IControl, public IBitmapBase
{
Expand Down
Loading
Loading