AnalogTapeModel

Physical modelling signal processing for analog tape recording
Log | Files | Refs | Submodules | README | LICENSE

commit 7533091a6558a49e6d347eb9b4ec01e607505f21
parent 4a0c87c46fdbffd55a93de4e8b7bf993da74cf75
Author: jatinchowdhury18 <[email protected]>
Date:   Fri,  5 Mar 2021 15:13:36 -0800

Add parameter to adjust playhead azimuth. (#149)

* Python sims

* Set up placeholder code for Azimuth

* Add azimuth processing

* {Apply clang-format}

Co-authored-by: jatinchowdhury18 <[email protected]>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Diffstat:
MPlugin/Source/GUI/Assets/gui.xml | 12++++++------
MPlugin/Source/Processors/CMakeLists.txt | 1+
APlugin/Source/Processors/Loss_Effects/AzimuthProc.cpp | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/Loss_Effects/AzimuthProc.h | 25+++++++++++++++++++++++++
MPlugin/Source/Processors/Loss_Effects/LossFilter.cpp | 7+++++++
MPlugin/Source/Processors/Loss_Effects/LossFilter.h | 3+++
ASimulations/LossEffects/azimuth.py | 31+++++++++++++++++++++++++++++++
7 files changed, 143 insertions(+), 6 deletions(-)

diff --git a/Plugin/Source/GUI/Assets/gui.xml b/Plugin/Source/GUI/Assets/gui.xml @@ -28,7 +28,7 @@ </Style> </Styles> <View id="root" resizable="1" resize-corner="1" flex-direction="column" - padding="0" width="580" height="580" background-color="FF8B3232" + padding="0" width="580" height="620" background-color="FF8B3232" background-image="Background_svg" image-placement="stretch"> <View max-height="100" padding="0" margin="0" background-color=""> <View margin="2" padding="" background-color="00000000" flex-direction="column" @@ -101,27 +101,27 @@ lookAndFeel="MyLNF"> <View flex-direction="column" tab-caption="Loss" tab-color="" background-color="FF31323A" padding="0" margin="0"> - <View flex-grow="0.05" background-color="00000000"/> <Slider caption="Gap [microns]" parameter="gap" slider-type="linear-horizontal" class="Slider" padding="0" slider-background="ff595c6b" slider-track="ff9cbcbd" name="Gap" tooltip="Sets the width of the playhead gap. Certain frequencies that resonate with the gap width will be emphasized." slidertext-height="18" caption-placement="top-left"/> - <View flex-grow="0.1" background-color="00000000"/> <Slider caption="Thickness [microns]" parameter="thick" class="Slider" slider-type="linear-horizontal" padding="0" slider-background="ff595c6b" slider-track="ff9cbcbd" name="Thickness" tooltip="Sets the thickness of the tape. Thicker tape has a more muted high-frequency response." caption-placement="top-left"/> - <View flex-grow="0.1" background-color="00000000"/> <Slider caption="Spacing [microns]" parameter="spacing" slider-type="linear-horizontal" class="Slider" padding="0" slider-background="ff595c6b" slider-track="ff9cbcbd" name="Spacing" tooltip="Sets the spacing between the tape and the playhead. A larger spacing means more high frequency signal is lost during playback." caption-placement="top-left"/> - <View flex-grow="0.1" background-color="00000000"/> + <Slider caption="Azimuth [degrees]" parameter="azimuth" slider-type="linear-horizontal" + class="Slider" padding="0" slider-background="ff595c6b" slider-track="ff9cbcbd" + name="Azimuth" tooltip="Sets the azimuth angle between the playhead and the tape. This can create a stereo widening effect at higher tape speeds." + caption-placement="top-left"/> <Slider caption="Speed [ips]" parameter="speed" slider-type="linear-horizontal" class="Slider" padding="0" slider-background="ff595c6b" slider-track="ff9cbcbd" name="Speed" tooltip="Sets the speed of the tape as it affects the playhead loss effects. Note that this control does not affect the wow/flutter processing." caption-placement="top-left"/> - <View flex-grow="0.57" margin="0" padding="2" background-color="00000000"> + <View flex-grow="0.53" margin="0" padding="2" background-color="00000000"> <TextButton margin="0" padding="2" text="3.75" button-color="00000000" background-color="00000000" onClick="set_speed_3.75" lookAndFeel="SpeedButtonLNF" button-on-color="00000000" name="3.75 ips" tooltip="Snaps the tape speed to 3.75 inches per second."/> diff --git a/Plugin/Source/Processors/CMakeLists.txt b/Plugin/Source/Processors/CMakeLists.txt @@ -8,6 +8,7 @@ target_sources(CHOWTapeModel PRIVATE Hysteresis/ToneControl.cpp Input_Filters/InputFilters.cpp + Loss_Effects/AzimuthProc.cpp Loss_Effects/LossFilter.cpp Timing_Effects/WowFlutterProcessor.cpp Timing_Effects/FlutterProcess.cpp diff --git a/Plugin/Source/Processors/Loss_Effects/AzimuthProc.cpp b/Plugin/Source/Processors/Loss_Effects/AzimuthProc.cpp @@ -0,0 +1,70 @@ +#include "AzimuthProc.h" + +namespace +{ +static constexpr float inches2meters (float inches) +{ + return inches / 39.370078740157f; +} + +static constexpr float deg2rad (float deg) +{ + return deg * MathConstants<float>::pi / 180.0f; +} + +constexpr float tapeWidth = inches2meters (0.25f); +} // namespace + +void AzimuthProc::prepare (double sampleRate, int samplesPerBlock) +{ + fs = (float) sampleRate; + + for (int ch = 0; ch < 2; ++ch) + { + delays[ch] = std::make_unique<ADelayLine> (1 << 18); + delays[ch]->prepare ({ sampleRate, (uint32) samplesPerBlock, 1 }); + + delaySampSmooth[ch].reset (sampleRate, 0.05); + } +} + +void AzimuthProc::setAzimuthAngle (float angleDeg, float tapeSpeedIps) +{ + const size_t delayIdx = size_t (angleDeg < 0.0f); + const auto tapeSpeed = inches2meters (tapeSpeedIps); + const auto azimuthAngle = deg2rad (std::abs (angleDeg)); + + auto delayDist = tapeWidth * std::sin (azimuthAngle); + auto delaySamp = (delayDist * tapeSpeed) * fs; + + delaySampSmooth[delayIdx].setTargetValue (delaySamp); + delaySampSmooth[1 - delayIdx].setTargetValue (0.0f); +} + +void AzimuthProc::processBlock (AudioBuffer<float>& buffer) +{ + if (buffer.getNumChannels() != 2) // needs to be stereo! + return; + + for (int ch = 0; ch < buffer.getNumChannels(); ++ch) + { + auto* x = buffer.getWritePointer (ch); + if (delaySampSmooth[ch].isSmoothing()) + { + for (int n = 0; n < buffer.getNumSamples(); ++n) + { + delays[ch]->setDelay (delaySampSmooth[ch].getNextValue()); + delays[ch]->pushSample (0, x[n]); + x[n] = delays[ch]->popSample (0); + } + } + else + { + for (int n = 0; n < buffer.getNumSamples(); ++n) + { + delays[ch]->pushSample (0, x[n]); + x[n] = delays[ch]->popSample (0); + } + } + } +} diff --git a/Plugin/Source/Processors/Loss_Effects/AzimuthProc.h b/Plugin/Source/Processors/Loss_Effects/AzimuthProc.h @@ -0,0 +1,25 @@ +#ifndef AZIMUTHPROC_H_INCLUDED +#define AZIMUTHPROC_H_INCLUDED + +#include <JuceHeader.h> + +class AzimuthProc +{ +public: + AzimuthProc() = default; + + void prepare (double sampleRate, int samplesPerBlock); + void setAzimuthAngle (float angleDeg, float tapeSpeedIps); + void processBlock (AudioBuffer<float>& buffer); + +private: + using ADelayLine = dsp::DelayLine<float, dsp::DelayLineInterpolationTypes::Lagrange3rd>; + std::unique_ptr<ADelayLine> delays[2]; + SmoothedValue<float, ValueSmoothingTypes::Linear> delaySampSmooth[2]; + + float fs = 48000.0f; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AzimuthProc) +}; + +#endif // AZIMUTHPROC_H_INCLUDED diff --git a/Plugin/Source/Processors/Loss_Effects/LossFilter.cpp b/Plugin/Source/Processors/Loss_Effects/LossFilter.cpp @@ -7,6 +7,7 @@ LossFilter::LossFilter (AudioProcessorValueTreeState& vts, int order) : order (o thickness = vts.getRawParameterValue ("thick"); gap = vts.getRawParameterValue ("gap"); onOff = vts.getRawParameterValue ("loss_onoff"); + azimuth = vts.getRawParameterValue ("azimuth"); for (int ch = 0; ch < 2; ++ch) { @@ -45,6 +46,8 @@ void LossFilter::createParameterLayout (std::vector<std::unique_ptr<RangedAudioP params.push_back (std::make_unique<AudioParameterFloat> ("thick", "Tape Thickness", thickRange, minDist, String(), AudioProcessorParameter::genericParameter, valueToString, stringToValue)); params.push_back (std::make_unique<AudioParameterFloat> ("gap", "Playhead Gap", gapRange, 1.0f, String(), AudioProcessorParameter::genericParameter, valueToString, stringToValue)); + + params.push_back (std::make_unique<AudioParameterFloat> ("azimuth", "Azimuth", -75.0f, 75.0f, 0.0f)); } float LossFilter::getLatencySamples() const noexcept @@ -85,6 +88,7 @@ void LossFilter::prepare (float sampleRate, int samplesPerBlock) prevThickness = *thickness; prevGap = *gap; + azimuthProc.prepare (sampleRate, samplesPerBlock); bypass.prepare (samplesPerBlock, bypass.toBool (onOff)); } @@ -196,5 +200,8 @@ void LossFilter::processBlock (AudioBuffer<float>& buffer) activeFilter = ! activeFilter; } + azimuthProc.setAzimuthAngle (azimuth->load(), speed->load()); + azimuthProc.processBlock (buffer); + bypass.processBlockOut (buffer, bypass.toBool (onOff)); } diff --git a/Plugin/Source/Processors/Loss_Effects/LossFilter.h b/Plugin/Source/Processors/Loss_Effects/LossFilter.h @@ -2,6 +2,7 @@ #define LOSSFILTER_H_INCLUDED #include "../BypassProcessor.h" +#include "AzimuthProc.h" #include "FIRFilter.h" class LossFilter @@ -34,6 +35,7 @@ private: std::atomic<float>* spacing = nullptr; std::atomic<float>* thickness = nullptr; std::atomic<float>* gap = nullptr; + std::atomic<float>* azimuth = nullptr; float prevSpeed; float prevSpacing; @@ -49,6 +51,7 @@ private: Array<float> currentCoefs; Array<float> Hcoefs; + AzimuthProc azimuthProc; BypassProcessor bypass; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LossFilter) diff --git a/Simulations/LossEffects/azimuth.py b/Simulations/LossEffects/azimuth.py @@ -0,0 +1,31 @@ +import numpy as np +import audio_dspy as adsp +import matplotlib.pyplot as plt + +def inches2meters(inches): + return inches / 39.370078740157 + +def deg2rad(deg): + return deg * np.pi / 180 + +tape_width = inches2meters(0.25) +tape_speed = inches2meters(15) +azimuth_angle = deg2rad(5) + +delay_dist = (tape_width / 2) * np.sin(azimuth_angle) +delay_ms = 1000 * (delay_dist / tape_speed) +print(delay_ms) + +FS = 48000 +delay_samp = (delay_ms / 1000) * FS +print(delay_samp) + +FILT_SAMP = delay_samp / 16 +x = np.arange(FILT_SAMP) # np.pi * np.arange(-FILT_SAMP, FILT_SAMP) / FILT_SAMP +p = 1 +h = -(6.0 / FILT_SAMP**3) * x * (x - FILT_SAMP) + +# plt.plot(h) +adsp.plot_magnitude_response(h, [1], fs=FS) +plt.ylim(-60) +plt.show()