AnalogTapeModel

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

commit b466a624876729b98b5a9384e1ce40cb479f644e
parent d12801ac7d7dfc0e22820e112586004211af73ae
Author: jatinchowdhury18 <jatinchowdhury18@users.noreply.github.com>
Date:   Fri, 15 Feb 2019 00:48:56 -0800

Implement tape loss effects

Diffstat:
MPaper/420_paper.pdf | 0
MPaper/420_paper.tex | 30++++++++++++++++++++++++++----
MPlugin/CHOWTapeModel.jucer | 8++++++++
MPlugin/Source/PluginProcessor.cpp | 8++++++++
MPlugin/Source/PluginProcessor.h | 2++
APlugin/Source/Processors/Loss Effects/LossEffects.cpp | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/Loss Effects/LossEffects.h | 35+++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/Loss Effects/LossEffectsFilter.cpp | 37+++++++++++++++++++++++++++++++++++++
APlugin/Source/Processors/Loss Effects/LossEffectsFilter.h | 25+++++++++++++++++++++++++
ASimulations/Loss_Effects.png | 0
ASimulations/Loss_Effects.py | 46++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 240 insertions(+), 4 deletions(-)

diff --git a/Paper/420_paper.pdf b/Paper/420_paper.pdf Binary files differ. diff --git a/Paper/420_paper.tex b/Paper/420_paper.tex @@ -242,7 +242,14 @@ described as follows \cite{Kadis}: \end{equation} % for sinusoidal input, where $k$ = wave number, $d$ is the distance between the tape and the playhead, -$g$ is the gap width of the play head, and $\delta$ is the thickness of the tape. +$g$ is the gap width of the play head, and $\delta$ is the thickness of the tape. The wave number +is given by: + +\begin{equation} + k = \frac {2 \pi f}{v} +\end{equation} +% +where $f$ is the frequency and $v$ is the tape speed. \section{Digitizing the System} \subsection{Record Head} @@ -332,7 +339,7 @@ meter, the following plot shows the Magnetisation output. \begin{figure}[ht] \center \includegraphics[width=3in]{../Simulations/Sim2-M_H.png} - \caption{\label{flowchart}{\it Digitized Hysteresis Loop Simulation}} + \caption{\label{HysteresisSim}{\it Digitized Hysteresis Loop Simulation}} \end{figure} % \subsection{Play Head} @@ -346,8 +353,23 @@ or, \hat{V}(n) = NWEv \mu_0 g \hat{M}(t) \end{equation} % -The loss effects described in \cref{eq:lossEffects} -can be modelled as a series of digital filters. +\subsubsection{Loss Effects} +In the real-time system, we model the playhead +loss effects with an FIR filter, derived by +taking the inverse DFT of the +loss effects described in \cref{eq:lossEffects}. +It is worth noting once again that the loss effects +and therefore the FIR filter as well, are dependent +on the tape speed. +The following plot shows the frequency response of +the loss effects for tape-head spacing of 20 microns, +head gap width of 5 microns, tape thickness of 35 microns, +and tape speed of 15 ips. +\begin{figure}[ht] + \center + \includegraphics[width=3in]{../Simulations/Loss_Effects.png} + \caption{\label{lossEffectsSim}{\it Frequency Response of Playhead Loss Effects}} +\end{figure} \subsection{Oversampling} To reduce frequency aliasing caused by the hysteresis non-linearity, diff --git a/Plugin/CHOWTapeModel.jucer b/Plugin/CHOWTapeModel.jucer @@ -8,6 +8,14 @@ <FILE id="mQFlPP" name="MyLNF.h" compile="0" resource="0" file="Source/GUI Extras/MyLNF.h"/> </GROUP> <GROUP id="{D2422983-A0E9-6A14-2092-2381CB1F3E7F}" name="Processors"> + <GROUP id="{99069F32-1399-FF98-7C4A-19F8E855C2AA}" name="Loss Effects"> + <FILE id="aEdcW7" name="LossEffects.cpp" compile="1" resource="0" file="Source/Processors/Loss Effects/LossEffects.cpp"/> + <FILE id="TFnfhT" name="LossEffects.h" compile="0" resource="0" file="Source/Processors/Loss Effects/LossEffects.h"/> + <FILE id="MSDyaq" name="LossEffectsFilter.cpp" compile="1" resource="0" + file="Source/Processors/Loss Effects/LossEffectsFilter.cpp"/> + <FILE id="C8T3dH" name="LossEffectsFilter.h" compile="0" resource="0" + file="Source/Processors/Loss Effects/LossEffectsFilter.h"/> + </GROUP> <GROUP id="{E3FB6B88-F1C2-FAF4-BE48-F8C36FEC858A}" name="Speed Filters"> <FILE id="rJ6Je0" name="SpeedFilterProcessor.cpp" compile="1" resource="0" file="Source/Processors/Speed Filters/SpeedFilterProcessor.cpp"/> diff --git a/Plugin/Source/PluginProcessor.cpp b/Plugin/Source/PluginProcessor.cpp @@ -29,6 +29,7 @@ ChowtapeModelAudioProcessor::ChowtapeModelAudioProcessor() tapeSpeed->addListener (this); speedFilter.setSpeed (*tapeSpeed); + lossEffects.setSpeed (*tapeSpeed); hysteresis.setOverSamplingFactor (*overSampling); } @@ -45,7 +46,10 @@ void ChowtapeModelAudioProcessor::parameterValueChanged (int paramIndex, float n else if (paramIndex == overSampling->getParameterIndex()) hysteresis.setOverSamplingFactor (*overSampling); else if (paramIndex == tapeSpeed->getParameterIndex()) + { speedFilter.setSpeed (*tapeSpeed); + lossEffects.setSpeed (*tapeSpeed); + } } //============================================================================== @@ -116,6 +120,7 @@ void ChowtapeModelAudioProcessor::prepareToPlay (double sampleRate, int samplesP inGainProc.prepareToPlay (sampleRate, samplesPerBlock); speedFilter.prepareToPlay (sampleRate, samplesPerBlock); hysteresis.prepareToPlay (sampleRate, samplesPerBlock); + lossEffects.prepareToPlay (sampleRate, samplesPerBlock); outGainProc.prepareToPlay (sampleRate, samplesPerBlock); } @@ -124,6 +129,7 @@ void ChowtapeModelAudioProcessor::releaseResources() inGainProc.releaseResources(); speedFilter.releaseResources(); hysteresis.releaseResources(); + lossEffects.releaseResources(); outGainProc.releaseResources(); } @@ -158,6 +164,8 @@ void ChowtapeModelAudioProcessor::processBlock (AudioBuffer<float>& buffer, Midi inGainProc.processBlock (buffer, midiMessages); hysteresis.processBlock (buffer, midiMessages); + + lossEffects.processBlock (buffer, midiMessages); speedFilter.processBlock (buffer, midiMessages); outGainProc.processBlock (buffer, midiMessages); diff --git a/Plugin/Source/PluginProcessor.h b/Plugin/Source/PluginProcessor.h @@ -4,6 +4,7 @@ #include "Processors/Hysteresis/HysteresisProcessor.h" #include "Processors/GainProcessor.h" #include "Processors/Speed Filters/SpeedFilterProcessor.h" +#include "Processors/Loss Effects/LossEffectsFilter.h" //============================================================================== /** @@ -60,6 +61,7 @@ public: private: SpeedFilterProcessor speedFilter; HysteresisProcessor hysteresis; + LossEffectsFilter lossEffects; GainProcessor inGainProc; GainProcessor outGainProc; diff --git a/Plugin/Source/Processors/Loss Effects/LossEffects.cpp b/Plugin/Source/Processors/Loss Effects/LossEffects.cpp @@ -0,0 +1,53 @@ +#include "LossEffects.h" + +LossEffects::LossEffects() +{} + +void LossEffects::init (float sampleRate, float speed) +{ + const auto numBins = sampleRate / (float) order; + const auto binWidth = sampleRate / numBins; + const auto speedMetric = speed * inchesToMeters; + + // Set freq domain multipliers + float H[order]; + for (int k = 0; k < order / 2; k++) + { + const auto freq = ((float) k * binWidth) + (binWidth / 2.0f); + const auto waveNumber = MathConstants<float>::twoPi * freq / speed; + + const auto spacingLoss = std::expf (-1.0f * waveNumber * spacing); + const auto gapLoss = std::sinf (waveNumber * gap / 2.0f) / (waveNumber * gap / 2.0f); + const auto thicknessLoss = (1.0f - std::expf (-waveNumber * thickness)) / (waveNumber * thickness); + + H[k] = spacingLoss * gapLoss * thicknessLoss; + H[order - k - 1] = H[k]; + } + + // Create time domain filter signals + for (int n = 0; n < order; n++) + { + h[n] = 0; + for (int k = 0; k < order; k++) + h[n] += H[k] * std::cosf (MathConstants<float>::twoPi * (float) k * (float) n / (float) order); + + h[n] /= (float) order; + } + + // Clear xs + xPtr = 0; + for (int n = 0; n < order; n++) + x[n] = 0; +} + +float LossEffects::process (float in) +{ + float y = 0.0f; + x[xPtr]= in; + for (int n = 0; n < order; n++) + y += h[n] * x[negativeAwareModulo<int> (xPtr - n, order)]; + + xPtr = (xPtr + 1) % order; + + return y; +} diff --git a/Plugin/Source/Processors/Loss Effects/LossEffects.h b/Plugin/Source/Processors/Loss Effects/LossEffects.h @@ -0,0 +1,35 @@ +#ifndef LOSSEFFECTS_H_INCLUDED +#define LOSSEFFECTS_H_INCLUDED + +#include "JuceHeader.h" + +namespace +{ + constexpr float spacing = (float) 20e-6; + constexpr float gap = (float) 5e-6; + constexpr float thickness = (float) 35e-6; + constexpr float inchesToMeters = 0.0254f; + + enum + { + order = 100, + }; +} + +class LossEffects +{ +public: + LossEffects(); + + void init (float sampleRate, float speed); + float process (float in); + +private: + float h[order]; + float x[order]; + int xPtr = 0; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LossEffects) +}; + +#endif //LOSSEFFECTS_H_INCLUDED diff --git a/Plugin/Source/Processors/Loss Effects/LossEffectsFilter.cpp b/Plugin/Source/Processors/Loss Effects/LossEffectsFilter.cpp @@ -0,0 +1,37 @@ +#include "LossEffectsFilter.h" + +void LossEffectsFilter::prepareToPlay (double sampleRate, int samplesPerBlock) +{ + setRateAndBufferSizeDetails (sampleRate, samplesPerBlock); + + lossProcessors[0].init ((float) sampleRate, speed); + lossProcessors[1].init ((float) sampleRate, speed); +} + +void LossEffectsFilter::releaseResources() +{ +} + +void LossEffectsFilter::processBlock (AudioBuffer<float>& buffer, MidiBuffer& /*midiBuffer*/) +{ + for (int channel = 0; channel < buffer.getNumChannels(); ++channel) + { + auto* x = buffer.getWritePointer (channel); + for (int n = 0; n < buffer.getNumSamples(); n++) + { + x[n] = lossProcessors[channel].process (x[n]); + } + } +} + +void LossEffectsFilter::setSpeed (String newSpeed) +{ + if (newSpeed == "3.75 ips") + speed = 3.75f; + else if (newSpeed == "7.5 ips") + speed = 7.5f; + else if (newSpeed == "15 ips") + speed = 15.0f; + + prepareToPlay (getSampleRate(), getBlockSize()); +} diff --git a/Plugin/Source/Processors/Loss Effects/LossEffectsFilter.h b/Plugin/Source/Processors/Loss Effects/LossEffectsFilter.h @@ -0,0 +1,25 @@ +#ifndef LOSSEFFECTSFILTER_H_INCLUDED +#define LOSSEFFECTSFILTER_H_INCLUDED + +#include "LossEffects.h" +#include "../ProcessorBase.h" + +class LossEffectsFilter : public ProcessorBase +{ +public: + LossEffectsFilter() : ProcessorBase ("Loss Effects Filter") {} + + void prepareToPlay (double sampleRate, int samplesPerBlock) override; + void releaseResources() override; + void processBlock (AudioBuffer<float>& buffer, MidiBuffer& /*midiBuffer*/) override; + + void setSpeed (String newSpeed); + +private: + LossEffects lossProcessors[2]; + float speed = 15.0f; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LossEffectsFilter) +}; + +#endif //LOSSEFFECTSFILTER_H_INCLUDED diff --git a/Simulations/Loss_Effects.png b/Simulations/Loss_Effects.png Binary files differ. diff --git a/Simulations/Loss_Effects.py b/Simulations/Loss_Effects.py @@ -0,0 +1,46 @@ +import numpy as np +import matplotlib.pyplot as plt +import scipy.signal as signal + +# Constants +N = 100 +d = 20e-6 #Spacing between tape and head +g = 5e-6 #Head gap width +delta = 35e-6 #Tape thickness +v = 15 * 0.0254 + +#f = np.linspace (0, 24000, N/2) +f = np.linspace (0, 48000, N) +n = np.linspace (0, N, N) +#n = np.linspace (0, N/2, N/2) + +# Calculate H(f) +H = np.zeros(N) +H[0] = 1 +waveNum = 2 * np.pi * f[1:int (N/2)] / v +#H[1:int (N/2)] = np.e ** (- abs(waveNum) * d) # Spacing loss +#H[1:int (N/2)] = np.sin (waveNum * g / 2) / (waveNum * g / 2) #gap loss +#H[1:int (N/2)] = (1 - np.exp (-waveNum * delta))/(waveNum * delta) #Thickness loss +H[1:int (N/2)] = (np.e ** (- abs(waveNum) * d)) * (np.sin (waveNum * g / 2) / (waveNum * g / 2)) * ((1 - np.exp (-waveNum * delta))/(waveNum * delta)) +H_flip = np.flip (H[0:int (N/2)], 0) +H[int (N/2):N] = H_flip + +# "Roll your own" iDFT +h = np.zeros (N) +for n_k in range (N): + for k in range (N): + h[n_k] += H[k] * np.cos (2 * np.pi * k * n_k / N) + h[n_k] *= (1/N) + +#h = np.fft.ifft (H) +H = np.fft.fft (h) +w, H_t = signal.freqz (h) + +# Plotting output +#plt.plot(n, h) +#plt.plot (w * 22000 / np.pi, abs (H_t)) +plt.semilogx (f[0:int (N/2)], 20 * np.log10 (H[0:int (N/2)])) +plt.title ("Tape Loss Effects vs. Frequency") +plt.xlabel ("Frequency [Hz]") +plt.ylabel ("Amplitude [dB]") +plt.show()