commit 6c1ff972ccc43c58c21e7f1d28faede48f15c153 parent d5a4d1ef176377a10e1ab1802c11de25de936d0c Author: jatinchowdhury18 <jatinchowdhury18@users.noreply.github.com> Date: Wed, 27 Feb 2019 21:20:05 -0800 Real-time optimization and refactoring/cleaning Diffstat:
34 files changed, 205 insertions(+), 553 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 @@ -113,7 +113,7 @@ \maketitle -\section{Abstract} +\begin{abstract} For decades, analog magnetic tape recording was the most popular method for recording music, but has been replaced over the past 30 years first by DAT tape, then by DAWs and audio interfaces \cite{Kadis}. Despite being replaced @@ -128,6 +128,7 @@ of a physical model of a Sony TC-260 tape machine. about a new medium will surely become its signature. CD distortion, the jitteriness of digital video, the crap sound of 8-bit - all of these will be cherished and emulated as soon as they can be avoided." -Brian Eno \cite{Eno}. +\end{abstract} \section{Continuous Time System} Audio recorded to and played back from a tape machine can be thought of as going @@ -309,7 +310,7 @@ Using the trapezoidal rule for derivative approximation, we find: \label{eq:hDeriv} \end{equation} % -We can use the Runge-Kutta 4th order method \cite{Yeh} to find an explicit solution +We can use the Runge-Kutta 4th-order method \cite{Yeh} to find an explicit solution for $\hat{M}(n)$: \begin{align} \begin{split} @@ -324,7 +325,7 @@ for $\hat{M}(n)$: % We use linear interpolation to find the half-sample values used to calculate $k_2$ and $k_3$. -\subsubsection{Numerical Considerations} +\subsubsection{Numerical Considerations} \label{numerical} To account for rounding errors in the Langevin function for values close to zero, we use the following approximation about zero, as in \cite{Hysteresis}: \begin{equation} @@ -339,13 +340,16 @@ zero, we use the following approximation about zero, as in \cite{Hysteresis}: \frac{1}{3} & \text{otherwise} \end{cases} \end{equation} - -\subsubsection{Simulation} -The digitized hysteresis loop was implemented and tested offline -in \texttt{Python}, using the constants $M_s$, $a$, $\alpha$, $k$, -and $c$ from \cite{JilesAtherton1986}. For a sinusoidal input signal -with frequency 2kHz, and varying amplitude from 800 - 2000 Amperes per -meter, the following plot shows the Magnetisation output. +% +Additionally, $\tanh(x)$, and by extension $\coth(x)$ is a +rather computationally expensive operation. With this in mind, +for real-time implementation, we approximate $\coth(x)$ as the +reciprocal of a Gaussian continued fraction for $\tanh(x)$ +\cite{MATH}, namely +\begin{equation} + \tanh(x) = \frac{x}{1 + \frac{x^2}{3 + \frac{x^2}{5 + \frac{x^2}{7}}}} + \label{eq:continuedFrac} +\end{equation} \begin{figure}[ht] \center @@ -353,6 +357,13 @@ meter, the following plot shows the Magnetisation output. \caption{\label{HysteresisSim}{\it Digitized Hysteresis Loop Simulation}} \end{figure} % +\subsubsection{Simulation} +The digitized hysteresis loop was implemented and tested offline +in \texttt{Python}, using the constants $M_s$, $a$, $\alpha$, $k$, +and $c$ from \cite{JilesAtherton1986}. For a sinusoidal input signal +with frequency 2kHz, and varying amplitude from 800 - 2000 Amperes per +meter, \cref{HysteresisSim} shows the Magnetisation output. + \subsection{Play Head} By combining \cref{eq:Vx} with \cref{eq12,eq15}, we get: \begin{equation} @@ -408,7 +419,7 @@ $27$ kA/m \cite{jilesBook}. \cite{jilesBook}, we can calculate $a$ = 22 kA/m by the method described in \cite{Jiles1992} \item Ratio of normal and hysteris initial susceptibilities ($c$): From \cite{Jiles1992}, $c$ = 1.7e-1. -\item Mean field parameter ($\alpha$): From \cite{Jiles1992}, alpha = 1.6e-3 +\item Mean field parameter ($\alpha$): From \cite{Jiles1992}, $\alpha$ = 1.6e-3 \end{itemize} \subsection{Tape Machine Parameters} @@ -453,6 +464,12 @@ head is highly variable between tape machines. For a typical tape machine spacing can be as high as 20 microns \cite{Kadis}. \end{itemize} +\begin{figure}[ht] + \center + \includegraphics[width=3in]{../Simulations/Bias/BiasEx.png} + \caption{\label{Bias}{\it Example of a biased signal}} +\end{figure} +% \subsection{Tape Bias} A typical analog recorder adds a high-frequency "bias" current to the signal to avoid the "deadzone" effect when the input signal @@ -466,16 +483,11 @@ current to the record head can be given by Where the amplitude of the bias current $B$ is usually about one order of magnitude larger than the input, and the bias frequency $f_{bias}$ is well above the -audible range. The plot below shows a unit-amplitude, +audible range. \Cref{Bias} shows a unit-amplitude, 2 kHz sine wave biased by a 50 kHz sine wave with amplitude 5. To recover the correct output signal, tape machines use a lowpass filter, with a cutoff frequency well below the bias frequency, thought still above the audible range \cite{Kadis}. -\begin{figure}[ht] - \center - \includegraphics[width=3in]{../Simulations/Bias/BiasEx.png} - \caption{\label{Bias}{\it Example of a biased signal}} -\end{figure} \newline\newline For the Sony TC-260, the bias frequency is 55 kHz, with a gain of 5 relative to the input signal. The lowpass filter used to recover @@ -508,23 +520,10 @@ signature "wow" effect of an analog tape machine. \caption{\label{timingSim}{\it Input pulse train superimposed with pulse train recorded from TC-260}} \end{figure} % - -\section{Real-Time Implementation} -We implemented our physical model of the Sony TC-260 as a -VST audio plugin using the JUCE framework. \Cref{flowchart} -shows the signal flow of the system in detail. We allow the user -to control ``normal'' parameters including the tape speed, -oversampling factor, bias frequency and bias gain. -Other ``unrealistic'' parameters were added, including -real-time control of gap width, tape thickness, tape spacing, -and flutter depth. C/C++ code for the real-time implementation -is available on GitHub -\footnote{\url{https://github.com/jatinchowdhury18/AnalogTapeModel}}. - \begin{figure*}[ht] \center \begin{tikzpicture} - \matrix (m1) [row sep=10mm, column sep=7.5mm] + \matrix (m1) [row sep=10mm, column sep=8mm] { & & @@ -532,7 +531,6 @@ is available on GitHub & & & - & \node[dspnodeopen,dsp/label=above] (b1) {Flutter ($\tau$)}; & \node[dspnodeopen,dsp/label=above] (b2) {Tape Speed ($v$)}; \\ %--------------------------------------------------------------- @@ -541,16 +539,15 @@ is available on GitHub \node[dspadder] (x2) {}; & \node[dspfilter] (x3) {$H_{record}$}; & \node[dspfilter] (x4) {Hysteresis}; & - \node[dspfilter] (x5) {De-bias}; & - \node[dspsquare] (x6) {\downsamplertext{M}}; & - \node[dspsquare] (x7) {$z^{-\tau}$}; & - \node[dspfilter] (x8) {$H_{play}(v)$}; & - \node[dspnodeopen,dsp/label=right] (x9) {$y[n]$}; \\ + \node[dspsquare] (x5) {\downsamplertext{M}}; & + \node[dspsquare] (x6) {$z^{-\tau}$}; & + \node[dspfilter] (x7) {$H_{play}(v)$}; & + \node[dspnodeopen,dsp/label=right] (x8) {$y[n]$}; \\ }; \draw[dspconn] (b0) -- (x2); - \draw[dspconn] (b1) -- (x7); - \draw[dspconn] (b2) -- (x8); - \foreach \i [evaluate = \i as \j using int(\i+1)] in {0,1,...,8} + \draw[dspconn] (b1) -- (x6); + \draw[dspconn] (b2) -- (x7); + \foreach \i [evaluate = \i as \j using int(\i+1)] in {0,1,...,7} \draw[dspconn] (x\i) -- (x\j); \end{tikzpicture} \caption{\label{flowchart}{\it Flowchart of realtime system: @@ -563,6 +560,16 @@ is available on GitHub de-biasing filter.}} \end{figure*} % +\section{Real-Time Implementation} +We implemented our physical model of the Sony TC-260 as a +VST audio plugin using the JUCE framework. \Cref{flowchart} +shows the signal flow of the system in detail. We allow the user +to control parameters in real-time including the tape speed, bias gain, gap +width, tape thickness, tape spacing, and flutter depth. +% C/C++ code for the real-time implementation is available on GitHub +% \footnote{\url{https://github.com/jatinchowdhury18/AnalogTapeModel}}. + +% \subsection{Oversampling} If no oversampling is used, the system will be unstable for input signal at the Nyquist frequency, due to limitations @@ -573,21 +580,31 @@ just below Nyquist. However, due to aliasing caused by the nonlinearity of the tape hysteresis model, oversampling is necessary to mitigate aliasing artifacts \cite{Yeh}. Further, the system must be able to faithfully recreate not only the -frequencies in the audible range but the bias frequencies as -well. Since the minimum standard audio sampling rate is 44.1 -kHz, and the minimum standard biasing frequency is approximately -50 kHz \cite{Camras:1987:MRH:27189}, a minimum oversampling -factor of 3x is required. In the real-time implementation, -we use a minimum oversampling factor of 4x, with options to -oversample at 8x or 16x if the user desires. - -\subsection{Performance} -The performance of the real time system is heavily -dependent on the oversampling factor. At an oversampling -factor of 2x or 4x, the system operates with a reasonable -CPU cost. At higher oversampling rates, the CPU cost quickly -becomes unreasonably large compared to a typical VST plugin. - +frequencies in the audible range but the bias frequency as +well. Since the TC-260 uses a bias frequency of 55 kHz \cite{RefManual} +and the minimum standard audio sampling rate is 44.1 kHz, +a minimum oversampling factor of 3x is required. However, +since the biased signal is then fed into the hystersis model, +even more oversampling is required to avoid aliasing. With these +considerations in mind, our system uses an oversampling +factor of 16x. + +% \subsection{Performance} +% At 16x oversampling, even with the various numerical optimizations +% described in \Cref{numerical}, the CPU load is fairly high. + +\begin{figure*}[!ht] + % \center + \includegraphics[width=2.4in]{../Testing/HysteresisTest.png} + \includegraphics[width=2.4in]{../Testing/DeadzoneTest.png} + \includegraphics[width=2.4in]{../Testing/FlutterTest.png} + \caption{\label{tests}{\it Testing results for real-time system: + input vs. output amplitude for variable + frequency sine wave (left), sine wave output + with no biasing (center), input vs. output + pulse train comparison (right).}} +\end{figure*} +% \subsection{Results} In subjective testing, our physical model sounds quite convincing, with warm, tape-like distortion, and realistic sounding @@ -596,31 +613,37 @@ flutter. The high-frequency loss and low-frequency and are approximately within the frequency response specifications of the TC-260 service manual \cite{RefManual}. The distortion and frequency response characteristics -of our model are subjectivelyvery close when compared to -the output of an actual TC-260. +of our model are subjectively very close when compared to +the output of an actual TC-260, though not nearly close enough to +``fool'' the listener. Additionally, as the bias +gain is lowered, the ``deadzone'' effect appears exactly +as expected \cite{Camras:1987:MRH:27189}. The largest difference between the model and the physical machine is the subtle electrical and mechanical noises and dropouts present in the physical machine, presumably caused by the age and wear-and-tear of the machine, which we did not attempt to characterize in our model. -\newline\newline -% TODO: Generate plots and sound examples from real-time system +\Cref{tests} shows the results +of tests performed on the real-time system. In particular, +note the successfully modelled hysteresis function (left), +the ``deadzone'' effect (center), and the timing imperfections +or flutter (right). Audio examples from the real-time +system can be found online\footnote{\url{https://ccrma.stanford.edu/~jatin/420/tape/}}. \section{Future Improvements} \subsection{Spatial Magnetic Effects} The most obvious improvement to be made for the physical model is the inclusion of spatial effects of the tape. In particular, -the approximation made in \cref{eq:spatialApprox}, negate any +the approximations made in \cref{eq:spatialApprox}, negate any effects caused by magnetisation along the longitudinal length of the tape, and into the depth of the tape. Including spatial effects would involve deriving digital analogues for \cref{eq:H_x,eq:H_y,eq:Vx}, and re-deriving \cref{eq:Mn} -to take an 2-dimensional magnetic field input per timestep, -rather than the zero-dimensional input. This change -would greatly increase the computational complexity of the system, -since solving \cref{eq:Mn} currently dominates the computational -complexity part of the system. Using an oversampling rate of 4x, -with just 100 spatial samples, would be 400x more +to take an 2-dimensional magnetic field input at every timestep, +rather than the zero-dimensional input it currently takes. This change +would greatly increase the computational complexity of the system. +At an oversampling rate of 16x, +using just 100 spatial samples would be 1600x more computationally complex than the current system. \section{Acknowledgements} diff --git a/Paper/references.bib b/Paper/references.bib @@ -106,3 +106,12 @@ booktitle = {Theory of Magnetic Recording, by H.~Neal Bertram, pp.~372.~ISBN 052 year = {2008}, url = {http://www.aes.org/e-lib/browse.cfm?elib=14800} } + +@Book{MATH, + title = {H. S. Wall}, + author = {Analytic Theory of Continued Fractions}, + year = {1948}, + publisher = {Chelsea}, + address = {New York} +} + diff --git a/Plugin/CHOWTapeModel.jucer b/Plugin/CHOWTapeModel.jucer @@ -37,23 +37,7 @@ <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"/> - <FILE id="vidpxP" name="SpeedFilterProcessor.h" compile="0" resource="0" - file="Source/Processors/Speed Filters/SpeedFilterProcessor.h"/> - </GROUP> <GROUP id="{E8FF20D6-35DC-19EF-2A0D-F8363CDBE1AE}" name="Hysteresis"> - <GROUP id="{06386EA3-C5F7-ED57-2B57-99637F9D3C99}" name="Bias Filter"> - <FILE id="TRBzap" name="BiasFilter.cpp" compile="1" resource="0" file="Source/Processors/Hysteresis/Bias Filter/BiasFilter.cpp"/> - <FILE id="Wc9DkH" name="BiasFilter.h" compile="0" resource="0" file="Source/Processors/Hysteresis/Bias Filter/BiasFilter.h"/> - <FILE id="rudm34" name="DCBlocker.cpp" compile="1" resource="0" file="Source/Processors/Hysteresis/Bias Filter/DCBlocker.cpp"/> - <FILE id="jfYcuq" name="DCBlocker.h" compile="0" resource="0" file="Source/Processors/Hysteresis/Bias Filter/DCBlocker.h"/> - <FILE id="nHz7MQ" name="FilterProcessor.cpp" compile="1" resource="0" - file="Source/Processors/Hysteresis/Bias Filter/FilterProcessor.cpp"/> - <FILE id="xwK1xF" name="FilterProcessor.h" compile="0" resource="0" - file="Source/Processors/Hysteresis/Bias Filter/FilterProcessor.h"/> - </GROUP> <FILE id="sjUZ6l" name="HysteresisProcessing.cpp" compile="1" resource="0" file="Source/Processors/Hysteresis/HysteresisProcessing.cpp"/> <FILE id="DrklCU" name="HysteresisProcessing.h" compile="0" resource="0" diff --git a/Plugin/Source/GUI Components/MainControls.cpp b/Plugin/Source/GUI Components/MainControls.cpp @@ -9,6 +9,7 @@ MainControls::MainControls (ChowtapeModelAudioProcessor& proc) : ChowtapeModelAudioProcessorEditor::createComboBox (oversampling, processor.overSampling, this); ChowtapeModelAudioProcessorEditor::createComboBox (tapeSpeed, processor.tapeSpeed, this); //ChowtapeModelAudioProcessorEditor::createComboBox (tapeType, processor.tapeType, this); + oversampling.setEnabled (false); ChowtapeModelAudioProcessorEditor::createLabel (inGainLabel, processor.inGain, this); ChowtapeModelAudioProcessorEditor::createLabel (outGainLabel, processor.outGain, this); diff --git a/Plugin/Source/PluginProcessor.cpp b/Plugin/Source/PluginProcessor.cpp @@ -64,14 +64,14 @@ ChowtapeModelAudioProcessor::~ChowtapeModelAudioProcessor() { } -void ChowtapeModelAudioProcessor::parameterValueChanged (int paramIndex, float newValue) +void ChowtapeModelAudioProcessor::parameterValueChanged (int paramIndex, float /*newValue*/) { if (paramIndex == inGain->getParameterIndex()) - inGainProc.setGain (Decibels::decibelsToGain (inGain->convertFrom0to1 (newValue))); + inGainProc.setGain (Decibels::decibelsToGain ((float) *inGain)); else if (paramIndex == outGain->getParameterIndex()) - outGainProc.setGain (Decibels::decibelsToGain (outGain->convertFrom0to1 (newValue))); - else if (paramIndex == overSampling->getParameterIndex()) - hysteresis.setOverSamplingFactor (*overSampling); + outGainProc.setGain (Decibels::decibelsToGain ((float) *outGain)); + //else if (paramIndex == overSampling->getParameterIndex()) + // hysteresis.setOverSamplingFactor (*overSampling); else if (paramIndex == tapeSpeed->getParameterIndex()) { lossEffects.setSpeed (*tapeSpeed); @@ -79,18 +79,18 @@ void ChowtapeModelAudioProcessor::parameterValueChanged (int paramIndex, float n } //else if (paramIndex == tapeType->getParameterIndex()) // return; //@TODO - else if (paramIndex == biasFreq->getParameterIndex()) - hysteresis.setBiasFreq (biasFreq->convertFrom0to1 (newValue)); + //else if (paramIndex == biasFreq->getParameterIndex()) + // hysteresis.setBiasFreq (*biasFreq); else if (paramIndex == biasGain->getParameterIndex()) - hysteresis.setBiasGain (biasGain->convertFrom0to1 (newValue)); + hysteresis.setBiasGain (*biasGain); else if (paramIndex == tapeSpacing->getParameterIndex()) - lossEffects.setSpacing (tapeSpacing->convertFrom0to1 (newValue)); + lossEffects.setSpacing (*tapeSpacing); else if (paramIndex == tapeThickness->getParameterIndex()) - lossEffects.setThickness (tapeThickness->convertFrom0to1 (newValue)); + lossEffects.setThickness (*tapeThickness); else if (paramIndex == gapWidth->getParameterIndex()) - lossEffects.setGap (gapWidth->convertFrom0to1 (newValue)); + lossEffects.setGap (*gapWidth); else if (paramIndex == flutterDepth->getParameterIndex()) - timingEffect.setDepth (flutterDepth->convertFrom0to1 (newValue)); + timingEffect.setDepth (*flutterDepth); } //============================================================================== diff --git a/Plugin/Source/PluginProcessor.h b/Plugin/Source/PluginProcessor.h @@ -65,7 +65,7 @@ public: AudioParameterFloat* flutterDepth; - void parameterValueChanged (int paramIndex, float newValue) override; + void parameterValueChanged (int paramIndex, float /*newValue*/) override; void parameterGestureChanged (int /*paramIndex*/, bool /*gestureIsStarting*/) override {} private: diff --git a/Plugin/Source/Processors/Hysteresis/Bias Filter/BiasFilter.cpp b/Plugin/Source/Processors/Hysteresis/Bias Filter/BiasFilter.cpp @@ -1,50 +0,0 @@ -#include "BiasFilter.h" - -BiasFilter::BiasFilter() : ProcessorBase ("Bias Filter") -{ - filter[0].setQ (0.5098f); - filter[1].setQ (0.6013f); - filter[1].setQ (0.8999f); - filter[1].setQ (2.5628f); -} - -void BiasFilter::setFreq (float newFreq) -{ - filter[0].setFreq (newFreq); - filter[1].setFreq (newFreq); - filter[2].setFreq (newFreq); - filter[3].setFreq (newFreq); -} - -void BiasFilter::processBlock (AudioBuffer<float>& buffer, MidiBuffer& midi) -{ - filter[0].processBlock (buffer, midi); - filter[1].processBlock (buffer, midi); - filter[2].processBlock (buffer, midi); - filter[3].processBlock (buffer, midi); - - dcBlocker.processBlock (buffer, midi); -} - -void BiasFilter::prepareToPlay (double sampleRate, int samplesPerBlock) -{ - setRateAndBufferSizeDetails (sampleRate, samplesPerBlock); - - filter[0].prepareToPlay (sampleRate, samplesPerBlock); - filter[1].prepareToPlay (sampleRate, samplesPerBlock); - filter[2].prepareToPlay (sampleRate, samplesPerBlock); - filter[3].prepareToPlay (sampleRate, samplesPerBlock); - - dcBlocker.prepareToPlay (sampleRate, samplesPerBlock); -} - -void BiasFilter::releaseResources() -{ - filter[0].releaseResources(); - filter[1].releaseResources(); - filter[2].releaseResources(); - filter[3].releaseResources(); - - dcBlocker.releaseResources(); -} - diff --git a/Plugin/Source/Processors/Hysteresis/Bias Filter/BiasFilter.h b/Plugin/Source/Processors/Hysteresis/Bias Filter/BiasFilter.h @@ -1,27 +0,0 @@ -#ifndef BIASFILTER_H_INCLUDED -#define BIASFILTER_H_INCLUDED - -#include "FilterProcessor.h" -#include "DCBlocker.h" - -/** 8th order Butterworth lowpass plus dc blocker */ -class BiasFilter : public ProcessorBase -{ -public: - BiasFilter(); - - void prepareToPlay (double sampleRate, int samplesPerBlock) override; - void releaseResources() override; - void processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiBuffer) override; - - void setFreq (float newFreq); - -private: - DCBlocker dcBlocker; - FilterProcessor filter[4]; - float freq = 24000.0f; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BiasFilter) -}; - -#endif //BIASFILTER_H_INCLUDED diff --git a/Plugin/Source/Processors/Hysteresis/Bias Filter/DCBlocker.cpp b/Plugin/Source/Processors/Hysteresis/Bias Filter/DCBlocker.cpp @@ -1,33 +0,0 @@ -#include "DCBlocker.h" - -void DCBlocker::prepareToPlay (double sampleRate, int samplesPerBlock) -{ - setRateAndBufferSizeDetails (sampleRate, samplesPerBlock); - - y1[0] = 0.0f; - y1[1] = 0.0f; - x1[0] = 0.0f; - x1[1] = 0.0f; -} - -void DCBlocker::releaseResources() -{ -} - -void DCBlocker::processBlock (AudioBuffer<float>& buffer, MidiBuffer& /*midiBuffer*/) -{ - for (int channel = 0; channel < buffer.getNumChannels(); channel++) - { - float* x = buffer.getWritePointer(channel); - for (int n = 0; n < buffer.getNumSamples(); n++) - x[n] = dcBlock (x[n], channel); - } -} - -float DCBlocker::dcBlock (float x, int ch) -{ - float y = x - x1[ch] + R * y1[ch]; - x1[ch] = x; - y1[ch] = y; - return y; -} diff --git a/Plugin/Source/Processors/Hysteresis/Bias Filter/DCBlocker.h b/Plugin/Source/Processors/Hysteresis/Bias Filter/DCBlocker.h @@ -1,25 +0,0 @@ -#ifndef DCBLOCKER_H_INCLUDED -#define DCBLOCKER_H_INCLUDED - -#include "../../ProcessorBase.h" - -class DCBlocker : public ProcessorBase -{ -public: - DCBlocker() : ProcessorBase ("DC Blocker") {} - - void prepareToPlay (double sampleRate, int samplesPerBlock) override; - void releaseResources() override; - void processBlock (AudioBuffer<float>& buffer, MidiBuffer&) override; - -private: - float dcBlock (float x, int ch); - - float y1[2] = { 0.0f, 0.0f }; - float x1[2] = { 0.0f, 0.0f }; - const float R = 0.995f; //@TODO: Tune this - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DCBlocker) -}; - -#endif //DCBLOCKER_H_INCLUDED diff --git a/Plugin/Source/Processors/Hysteresis/Bias Filter/FilterProcessor.cpp b/Plugin/Source/Processors/Hysteresis/Bias Filter/FilterProcessor.cpp @@ -1,105 +0,0 @@ -#include "FilterProcessor.h" - -void FilterProcessor::initFilter() -{ - resetArrays(); - calcCoefs(); - clearIOs(); -} - -void FilterProcessor::resetArrays() -{ - a.reset (new float[order + 1]); - b.reset (new float[order + 1]); - - const int numChannels = 2; //getTotalNumOutputChannels(); - filterIOs.reset (new std::unique_ptr<float[]> [numChannels]); - for (int ch = 0; ch < numChannels; ch++) - filterIOs[ch].reset (new float[order * 2]); -} - -void FilterProcessor::clearIOs() -{ - for (int ch = 0; ch < 2; ch++) - for (int samp = 0; samp < order * 2; samp++) - filterIOs[ch][samp] = 0.0f; -} - -void FilterProcessor::calcCoefs() -{ - float alpha = tanf (MathConstants<float>::pi * cutoffFreq / (float) getSampleRate()); - float alphaSquared = alpha * alpha; - float K = alphaSquared + alpha / Q + 1.0f; - - // b0, b1, b2 for setCoefs() - float coef0 = alphaSquared * gain / K; - float bs[] = { coef0, // *x0 - 2.0f * coef0, // *x1 - coef0, // *x2 - }; - - // a0, a1, a2 for setCoefs() - float coef1 = (2.0f * (alphaSquared - 1.0f)) / K; - float coef2 = (alphaSquared - alpha/Q + 1.0f) / K; - float as[] = { 1.0f, // *y0 (usually 1) - coef1, // *y1 - coef2 // *y2 - }; - - setCoefs(as, bs); -} - -void FilterProcessor::setCoefs (float* as, float* bs) -{ - for (int i = 0; i < order + 1; i++) - { - a[i] = as[i]; - b[i] = bs[i]; - } -} - -void FilterProcessor::prepareToPlay (double sampleRate, int maxExpectedBlockSize) -{ - setRateAndBufferSizeDetails (sampleRate, maxExpectedBlockSize); - initFilter(); -} - -void FilterProcessor::releaseResources() -{ - clearIOs(); -} - -void FilterProcessor::setFreq (float newFreq) -{ - cutoffFreq = newFreq; - calcCoefs(); -} - -void FilterProcessor::setQ (float newQ) -{ - Q = newQ; - calcCoefs(); -} - -void FilterProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& /*midiBuffer*/) -{ - for (int channel = 0; channel < buffer.getNumChannels(); channel++) - { - float* x = buffer.getWritePointer(channel); - for (int n = 0; n < buffer.getNumSamples(); n++) - x[n] = filter (channel, x[n]); - } -} - -float FilterProcessor::filter (int ch, float x) -{ - float y = b[0] * x + b[1] * filterIOs[ch][0] + b[2] * filterIOs[ch][1] - - a[1] * filterIOs[ch][2] - a[2] * filterIOs[ch][3]; - - //update I/O's - filterIOs[ch][3] = filterIOs[ch][2]; //y2 = y1 - filterIOs[ch][1] = filterIOs[ch][0]; //x2 = x1 - filterIOs[ch][2] = y; //y1 = y - filterIOs[ch][0] = x; //x1 = x - return y; -} diff --git a/Plugin/Source/Processors/Hysteresis/Bias Filter/FilterProcessor.h b/Plugin/Source/Processors/Hysteresis/Bias Filter/FilterProcessor.h @@ -1,47 +0,0 @@ -#ifndef FILTERPROCESSOR_H_INCLUDED -#define FILTERPROCESSOR_H_INCLUDED - -#include "../../ProcessorBase.h" - -class FilterProcessor : public ProcessorBase -{ -public: - enum - { - order = 2, - maxFreq = 22000, - farFreq = 1000, - }; - - FilterProcessor() : ProcessorBase (String ("Filter Processor")) { initFilter(); } - - void prepareToPlay (double sampleRate, int maxExpectedBlockSize) override; - void releaseResources() override; - void processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiBuffer) override; - - void setFreq (float newFreq); - void setQ (float newQ); - float setFreq() const { return cutoffFreq; } - -private: - void initFilter(); - void clearIOs(); - void resetArrays(); - - void calcCoefs(); - void setCoefs (float* as, float* bs); - - float filter (int channel, float x); - - float cutoffFreq = (float) maxFreq; - float Q = 0.707f; - const float gain = 1.0f; - - std::unique_ptr<float[]> a; // a0, a1, a2 (a0 is usually 1) - std::unique_ptr<float[]> b; // b0, b1, b2 - std::unique_ptr<std::unique_ptr<float[]> []> filterIOs; //x1, x2, y1, y2 - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FilterProcessor) -}; - -#endif // !FILTERPROCESSOR_H_INCLUDED diff --git a/Plugin/Source/Processors/Hysteresis/HysteresisProcessing.cpp b/Plugin/Source/Processors/Hysteresis/HysteresisProcessing.cpp @@ -6,17 +6,17 @@ HysteresisProcessing::HysteresisProcessing() } -float HysteresisProcessing::tanhApprox (float x) +float HysteresisProcessing::cothApprox (float x) { const auto xSquare = x * x; - return x / (1.0f + (xSquare / (3.0f + (xSquare / (5.0f + (xSquare / (7.0f))))))); + return (1.0f + (xSquare / (3.0f + (xSquare / (5.0f + (xSquare / (7.0f))))))) / x; } float HysteresisProcessing::langevin (float x) { if (nearZero) - return (tanhRecip) - (1.0f / x); + return (coth) - (1.0f / x); else return (x / 3.0f); } @@ -24,7 +24,7 @@ float HysteresisProcessing::langevin (float x) float HysteresisProcessing::langevinD (float x) { if (nearZero) - return (1.0f / (x * x)) - (tanhRecip * tanhRecip) + 1.0f; + return (1.0f / (x * x)) - (coth * coth) + 1.0f; else return (1.0f / 3.0f); } @@ -37,8 +37,7 @@ float HysteresisProcessing::deriv (float x_n, float x_n1, float x_d_n1) float HysteresisProcessing::hysteresisFunc (float M, float H, float H_d) { const float Q = (H + alpha * M) / a; - tanHyp = (float) tanhApprox (Q); - tanhRecip = 1.0f / tanHyp; + coth = cothApprox (Q); nearZero = std::abs (Q) > (float) (10e-4); const float M_diff = M_s * langevin (Q) - M; diff --git a/Plugin/Source/Processors/Hysteresis/HysteresisProcessing.h b/Plugin/Source/Processors/Hysteresis/HysteresisProcessing.h @@ -13,7 +13,7 @@ public: void setSampleRate (float newSR) { fs = newSR; T = 1.0f / fs; } private: - float tanhApprox (float x); + float cothApprox (float x); float langevin (float x); float langevinD (float x); @@ -34,8 +34,9 @@ private: float H_n1 = 0.0f; float H_d_n1 = 0.0f; - float tanHyp = 0.0f; - float tanhRecip = 0.0f; + float coth = 0.0f; + // float tanHyp = 0.0f; + // float tanhRecip = 0.0f; bool nearZero = false; //JUCE_DECLARE_NONCOPYABLE_WITH_LEAK_DETECTOR (HysteresisProcessing) diff --git a/Plugin/Source/Processors/Hysteresis/HysteresisProcessor.h b/Plugin/Source/Processors/Hysteresis/HysteresisProcessor.h @@ -3,7 +3,6 @@ #include "../ProcessorBase.h" #include "HysteresisProcessing.h" -#include "Bias Filter/BiasFilter.h" class HysteresisProcessor : public ProcessorBase { diff --git a/Plugin/Source/Processors/Speed Filters/SpeedFilterProcessor.cpp b/Plugin/Source/Processors/Speed Filters/SpeedFilterProcessor.cpp @@ -1,112 +0,0 @@ -#include "SpeedFilterProcessor.h" - -void SpeedFilterProcessor::initFilter() -{ - resetArrays(); - calcCoefs(); - clearIOs(); -} - -void SpeedFilterProcessor::resetArrays() -{ - a.reset (new float[order + 1]); - b.reset (new float[order + 1]); - - const int numChannels = 2; //getTotalNumOutputChannels(); - filterIOs.reset (new std::unique_ptr<float[]> [numChannels]); - for (int ch = 0; ch < numChannels; ch++) - filterIOs[ch].reset (new float[order * numChannels]); -} - -void SpeedFilterProcessor::clearIOs() -{ - for (int ch = 0; ch < 2 /*getTotalNumOutputChannels()*/; ch++) - for (int samp = 0; samp < 4; samp++) - filterIOs[ch][samp] = 0.0f; -} - -void SpeedFilterProcessor::calcCoefs() -{ - float alpha = tanf (MathConstants<float>::pi * cutoffFreq / (float) getSampleRate()); - float alphaSquared = alpha * alpha; - float K = alphaSquared + alpha / Q + 1.0f; - - // b0, b1, b2 for setCoefs() - float coef0 = alphaSquared * gain / K; - float bs[] = { coef0, // *x0 - 2.0f * coef0, // *x1 - coef0, // *x2 - }; - - // a0, a1, a2 for setCoefs() - float coef1 = (2.0f * (alphaSquared - 1.0f)) / K; - float coef2 = (alphaSquared - alpha/Q + 1.0f) / K; - float as[] = { 1.0f, // *y0 (usually 1) - coef1, // *y1 - coef2 // *y2 - }; - - setCoefs(as, bs); -} - -void SpeedFilterProcessor::setCoefs (float* as, float* bs) -{ - for (int i = 0; i < order + 1; i++) - { - a[i] = as[i]; - b[i] = bs[i]; - } -} - -void SpeedFilterProcessor::prepareToPlay (double sampleRate, int maxExpectedBlockSize) -{ - setRateAndBufferSizeDetails (sampleRate, maxExpectedBlockSize); - initFilter(); -} - -void SpeedFilterProcessor::releaseResources() -{ - clearIOs(); -} - -void SpeedFilterProcessor::setSpeed (String speed) -{ - if (speed == "3.75 ips") - setFreq (12000); - else if (speed == "7.5 ips") - setFreq (15000); - else if (speed == "15 ips") - setFreq (18000); -} - -void SpeedFilterProcessor::setFreq (float newFreq) -{ - cutoffFreq = newFreq; - calcCoefs(); -} - -void SpeedFilterProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& /*midiBuffer*/) -{ - for (int channel = 0; channel < buffer.getNumChannels(); channel++) - { - float* x = buffer.getWritePointer(channel); - for (int n = 0; n < buffer.getNumSamples(); n++) - x[n] = filter (channel, x[n]); - } -} - -float SpeedFilterProcessor::filter (int ch, float x) -{ - float y = b[0] * x + b[1] * filterIOs[ch][0] + b[2] * filterIOs[ch][1] - - a[1] * filterIOs[ch][2] - a[2] * filterIOs[ch][3]; - - //update I/O's - filterIOs[ch][3] = filterIOs[ch][2]; //y2 = y1 - filterIOs[ch][1] = filterIOs[ch][0]; //x2 = x1 - filterIOs[ch][2] = y; //y1 = y - filterIOs[ch][0] = x; //x1 = x - - jassert (abs(y) < 1.0f); - - return y; -} diff --git a/Plugin/Source/Processors/Speed Filters/SpeedFilterProcessor.h b/Plugin/Source/Processors/Speed Filters/SpeedFilterProcessor.h @@ -1,47 +0,0 @@ -#ifndef SPEEDFILTERPROCESSOR_H_INCLUDED -#define SPEEDFILTERPROCESSOR_H_INCLUDED - -#include "../ProcessorBase.h" - -class SpeedFilterProcessor : public ProcessorBase -{ -public: - enum - { - order = 2, - maxFreq = 22000, - }; - - SpeedFilterProcessor() : ProcessorBase (String ("Filter Processor")) { initFilter(); } - - void prepareToPlay (double sampleRate, int maxExpectedBlockSize) override; - void releaseResources() override; - void processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiBuffer) override; - - void setSpeed (String speed); - - void setFreq (float newFreq); - float getFreq() const { return cutoffFreq; } - -private: - void initFilter(); - void clearIOs(); - void resetArrays(); - - void calcCoefs(); - void setCoefs (float* as, float* bs); - - float filter (int channel, float x); - - float cutoffFreq = (float) maxFreq; - const float Q = 0.707f; - const float gain = 1.0f; - - std::unique_ptr<float[]> a; // a0, a1, a2 (a0 is usually 1) - std::unique_ptr<float[]> b; // b0, b1, b2 - std::unique_ptr<std::unique_ptr<float[]> []> filterIOs; //x1, x2, y1, y2 - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SpeedFilterProcessor) -}; - -#endif // !SPEEDFILTERPROCESSOR_H_INCLUDED diff --git a/Plugin/Source/Processors/Timing Effects/TimingEffect.cpp b/Plugin/Source/Processors/Timing Effects/TimingEffect.cpp @@ -23,7 +23,8 @@ double TimingEffect::getTailLengthSeconds() const void TimingEffect::setLength (int channel, int lengthSamples) { - int newLength = jmax (0, jmin (lengthSamples, (int) maxDelaySamples)); + auto corrLength = roundToInt ((double) lengthSamples * getSampleRate() / fs_calc); + int newLength = jmax (0, jmin (corrLength, (int) maxDelaySamples)); dChannels[channel].length.setValue ((float) newLength); } diff --git a/Plugin/Source/Processors/Timing Effects/TimingEffect.h b/Plugin/Source/Processors/Timing Effects/TimingEffect.h @@ -23,10 +23,11 @@ public: private: enum { - maxDelaySamples = 600 * 10, + maxDelaySamples = 600 *25, polyOrder = 10, periodLen = 490, }; + static constexpr double fs_calc = 44100.0; //Flutter calculations originally done at this sample rate struct DelayChannel { diff --git a/Testing/Canada_Bias.wav b/Testing/Canada_Bias.wav Binary files differ. diff --git a/Testing/Canada_Dry.wav b/Testing/Canada_Dry.wav Binary files differ. diff --git a/Testing/Canada_NoBias.wav b/Testing/Canada_NoBias.wav Binary files differ. diff --git a/Testing/DeadzoneTest.png b/Testing/DeadzoneTest.png Binary files differ. diff --git a/Testing/FlutterTest.png b/Testing/FlutterTest.png Binary files differ. diff --git a/Testing/HysteresisTest.png b/Testing/HysteresisTest.png Binary files differ. diff --git a/Testing/PulseTrain_Out.wav b/Testing/PulseTrain_Out.wav Binary files differ. diff --git a/Testing/RisingSine_Bias.wav b/Testing/RisingSine_Bias.wav Binary files differ. diff --git a/Testing/RisingSine_Dry.wav b/Testing/RisingSine_Dry.wav Binary files differ. diff --git a/Testing/RisingSine_NoBias.wav b/Testing/RisingSine_NoBias.wav Binary files differ. diff --git a/Testing/deadzone_test.py b/Testing/deadzone_test.py @@ -0,0 +1,23 @@ +import numpy as np +import matplotlib.pyplot as plt +import soundfile as sf + +def toMono (arr): + list = [] + for samp in arr: + list.append (samp[0]) + return list + +data, fs = sf.read ("RisingSine_NoBias.wav") +mono = toMono (data) + +N = 450 +start = 5900 + +t = np.arange (len (mono)) + +plt.plot (t[start:start+N], mono[start:start+N]) +plt.xlabel ("Time [samples]") +plt.ylabel ("Output Amplitude") +plt.title ("\"Deadzone\" Effect") +plt.show() diff --git a/Testing/pulse_train.wav b/Testing/pulse_train.wav Binary files differ. diff --git a/Testing/sine_test.py b/Testing/sine_test.py @@ -0,0 +1,32 @@ +import numpy as np +import matplotlib.pyplot as plt +import soundfile as sf + +def toMono (arr): + list = [] + for samp in arr: + list.append (samp[0]) + return list + + +d, fs = sf.read ("RisingSine_Dry.wav") + +# just use left channel, signal is mono anyway +dry = toMono (-1 * d) + +b, fs = sf.read("RisingSine_Bias.wav") +bias = toMono (b) + +n, fs = sf.read("RisingSine_NoBias.wav") +noBias = toMono (n) + +N = len(dry) +t = np.arange (N) + +# print (N) + +plt.plot (dry[0:20000], bias[0:20000]) +plt.xlabel ("Input Amplitude") +plt.ylabel ("Output Amplitude") +plt.title ("Real-time System Hysteresis Loop") +plt.show() diff --git a/Testing/timing_test.py b/Testing/timing_test.py @@ -0,0 +1,25 @@ +import numpy as np +import matplotlib.pyplot as plt +import soundfile as sf + +def toMono (arr): + list = [] + for samp in arr: + list.append (samp[0]) + return list + +pulse_in, fs = sf.read ("pulse_train.wav") + +pulse_out, fs = sf.read ("PulseTrain_Out.wav") +pulse_out = toMono (pulse_out) + +N = len (pulse_in) +t = np.arange (N) +n = 60000 +start = 30000 + +plt.plot (t[start:start+n], pulse_in[start:start+n], t[start:start+n], pulse_out[start:start+n]) +plt.xlabel ("Time [samples]") +plt.ylabel ("Amplitude") +plt.title ("Real-time Timing Offset (Flutter)") +plt.show()