AUDIO PROGRAMMING 2 PING PONG DELAY
Rachel Locke School of Computing and Engineering Digital Audio Engineering Stephen Oxnard June 2021
2
Table of Contents INTRODUCTION ........................................................................................................................ 3 METHOD................................................................................................................................... 3 MATLAB PROTOTYPE.......................................................................................................................... 3 JUCE PLUGIN ...................................................................................................................................... 6
RESULTS AND DISCUSSION ....................................................................................................... 8 CONCLUSION .......................................................................................................................... 15 REFERENCES ........................................................................................................................... 18 APPENDIX ............................................................................................................................... 19
List of Figures Figure 1. Ping Pong Delay Effect Figure 2. Input Signal Figure 3. Base Case Ping Pong Delay Algorithm MATLAB Prototype (πππππ¦πΌππππππππ = 20) Figure 4. Base Case Ping Pong Delay Algorithm C++ Render (πππππ¦πΌππππππππ = 20) Figure 5. Various Feedback Values MATLAB Prototype (Left Channel) (πππππ¦πΌππππππππ = 20) Figure 6. Various Feedback Values C++ Render (Left Channel) (πππππ¦πΌππππππππ = 20) Figure 7. Various Feedback Values MATLAB Prototype (Right Channel) (πππππ¦πΌππππππππ = 20) Figure 8. Various Feedback Values C++ Render (Left Channel) (πππππ¦πΌππππππππ = 20) Figure 9. 1/8th Note Delay 100bpm C++ Render (First Sample Only) (πππππ¦πΌππππππππ = 14400) Figure 10. 1/4 Note Delay 100bpm C++ Render (First Sample Only) (πππππ¦πΌππππππππ = 28800) Figure 11. 1/2 Note Delay 100bpm C++ Render (First Sample Only) (πππππ¦πΌππππππππ = 57600) Figure 12. 1/8, 1/4, and 1/2 Note Delay 100bpm C++ Render (Left Channel) Figure 13. 1/8, 1/4, and 1/2 Note Delay 100bpm C++ Render (Right Channel) Figure 14. Modulation Rate for Delay time as a Function of Time (Case, 2007) Figure 15. Modulation Depth for Delay time as a Function of Time (Case, 2007) Figure 16. Modulation Shape for Delay time as a Function of Time (Case, 2007)
3
INTRODUCTION
Figure 1. Ping Pong Delay Effect (Pirkle, 2019) The ping pong delay algorithms used in this paper are adapted from the above diagram (fig 1), taken from the second edition of Pirkleβs book, βDesigning Audio Effect Plugins in C++β. Delay effects are a class of memory-using effect categorised by their use of delay lines. The ping pong delay is a type of stereo echo effect characterised by its crossed input and feedback paths i.e. the right channelβs input is fed into the left channelβs delay block and the delayed output of the left channel is fed back as an input to the right channelβs delay block and vice versa. This creates an audible βping pongβ effect as the delays bounce from left to right in a repetitive fashion (Tarr, 2019). This project consists of a MATLAB prototype and an equivalent C++/JUCE VST3 plugin. Both algorithms allow the following: β’ β’ β’
Dry/Wet control Feedback control Delay length control (1/8th, 1/4, and 1/2 note tempo-synced delay)
In this case, the feedback, dry/wet mix and delay length are identical for each channel and set with the same variables and GUI control. METHOD MATLAB PROTOTYPE
In MATLAB the length of the simulation is specified at the beginning of the program. This value determines the length of the input signal in samples and the iterations of the main time loop. N = 2000; %length of simulation in samples
4
Impulse functions make ideal test signals for delay line-based effects as they give a clear visual indication in the time domain of delay length and feedback (fig 3). The behaviour of a ping pong delay can be demonstrated and therefore tested with a stereo input. Here, the left channel is a regular impulse function, whilst the right channel is a delayed, attenuated impulse function. Delaying and attenuating one of the impulses makes it easier to differentiate between the left and right input when studying the plots of the left and right output channels. x = zeros(N,2); % Initialise input stereo array x(1,1) = 1; % Left channel impulse function x(5,2) = 0.4; % Right channel impulse function (delayed and attenuated)
Delay coefficients that determine the character of the delay effect are hardcoded in the MATLAB prototype, later to become GUI controls in the final JUCE plugin. dryWet = 0.5; %dry value, wet value = 1 - dryWet feedback = 0.7; % feedback value delayInSeconds = 1; %seconds of delay delayInSamples = delayInSeconds * Fs; %samples of delay
Stereo echo effects require two delay lines, one belonging to each channel. This could be achieved with an π by 2 matrix in the same way that the input samples are stored. However, here they are declared as distinct left and right buffers to demonstrate the distinction of the two. bufferSize = Fs * 3; %buffer length in samples bufferLeft = zeros(bufferSize, 1); %left delay line bufferRight = zeros(bufferSize, 1); %right delay line
The stereo output is declared as two vectors, one for each output channel, and initialised as zeros. These will later be populated with output samples from the left and right delay lines in the main time loop. % output vectors leftOutput = zeros(samples, 1); % left channel out rightOutput = zeros(samples, 1); % right channel out
The modification made to the base case algorithm (fig 1) is the synchronization of the delay time with the tempo of the input track. This gives the ping pong delay a rhythmic element and allows the user to select their delay time from four different note lengths, 1/4 note (crotchet), 1/8th note (quaver), dotted 1/8th note, and 1/2 note (minim), calculated based on the variable beatsPerMinute. The delay time is ultimately determined by the variable noteLength which multiplies secondsPerBeat * Fs to achieve the delay in samples based on one of the note length choices. The MATLAB rounding function fix() is used here to round to the nearest integer towards zero. This ensures that the delay in samples is a whole number as this value will be used as a write index in the main time loop. beatsPerMinute = 100; beatsPerSecond = beatsPerMinute/60; %find seconds per beat by taking the inverse secondsPerBeat = 1/beatsPerSecond; %note length should be user controllable in final plugin noteLength = 1; %0.5 = 1/8th, 0.75 = dotted 1/8th, 1 = 1/4, 2 = 1/2 delayInSamples = fix(noteLength * secondsPerBeat * Fs);
This prototype uses distinct read and write heads as opposed to a combined read-write index in order to use the full length of the delay lines and provide clarity regarding where the reading and writing is happening.
5 delayReadIndex = 1; %read head (index to read from buffers) delayWriteIndex = delayInSamples; %write head (index to write to buffers)
Main time loop
Once the delay coefficients, delay lines, and stereo output array have been initialised, the ping pong delay algorithm can be implemented in a main time loop that iterates over the samples n = 1:samples. The first step in the process is reading the samples from the input. For each channel, the current input sample n is stored as a variable for later use. % read samples from input leftChannel = x(n,1); % current input sample (left channel) rightChannel = x(n,2); % current input sample (right channel)
A conditional then determines whether the read and write indexes are in range with regards to the buffer size (length of the delay lines). When these indexes exceed the buffer size, the buffer size is subtracted from the value, returning the read or write index to the beginning of the buffer and therefore creating a ring buffer. % make buffers circular if (delayReadIndex > bufferSize) delayReadIndex = delayReadIndex - bufferSize; end if (delayWriteIndex > bufferSize) delayWriteIndex = delayWriteIndex - bufferSize; end
The current outputs of the left and right delay lines are stored to temporary variables to prevent them from being overwritten in the process of implementing the ping pong effect. The current delay line outputs are recorded at the current delayReadIndex. %get the current output of the circular buffers delayOutLeft = bufferLeft(delayReadIndex); delayOutRight = bufferRight(delayReadIndex);
The sum of the opposite channelβs current input sample and the opposite channelβs current feedback sample form the input for each delay block. π₯(π)! = : π₯(π)" + ππ(π)" π₯(π)" = : π₯(π)! + ππ(π)! % Left delay block input delayInLeft = rightChannel + feedback * delayOutRight; % Right delay block input delayInRight = leftChannel + feedback * delayOutLeft;
The delayInLeft and delayInRight values can then to written to their corresponding delay line at the delayWriteIndex. %fill buffers with delayed signal bufferLeft(delayWriteIndex) = delayInLeft; bufferRight(delayWriteIndex) = delayInRight;
From here the output values y(n) are calculated and assigned to their corresponding left and right channels. This is achieved in two steps for clarity. First, the output values for each channel are calculated
6 and assigned to temporary variables leftChannelOut and rightChannelOut. The output channels consist of the current corresponding input value multiplied by a dry coefficient (dryWet) summed with the current corresponding delay line output value multiplied by a wet coefficient (1dryWet). Secondly, the variables leftChannelOut and rightChannelOut are assigned to y(n,1) and y(n,2) to fill the stereo output array. %output sample calculation leftChannelOut = dryWet * leftChannel + (1-dryWet) * delayOutLeft; rightChannelOut = dryWet * rightChannel + (1-dryWet) * delayOutRight; %fill output channels y(n,1) = leftChannelOut; y(n,2) = rightChannelOut;
Finally, the delayReadIndex and delayWriteIndex are incremented moving them forward one position in the delay buffers until they go out of range and are wrapped back to the beginning. %increment read and write delayReadIndex = delayReadIndex + 1; delayWriteIndex = delayWriteIndex + 1; JUCE PLUGIN
JUCE is a C++ framework written by Julian Storer, optimized for the development of audio applications and plugins, due in part to its extensive audio-related classes (Pirkle, 2019). This project makes use of the JUCE framework to develop a VST3 plugin, a useable audio signal processing tool with GUI user control that can be βplugged inβ to a Digital Audio Workstation (DAW) to extend its capabilities. The ping pong delay JUCE plugin is largely based on the MATLAB prototype, however, there are some specific modifications made in the conversion from MATLAB to C++ code. One of the more significant differences between the two involves the left and right delay lines. In C++ the memory for these buffers is dynamically allocated on the heap and therefore, to avoid memory leaks, needs to be deleted. This process is made possible via the C++ keywords new and delete. The float* types that will become the delay lines are declared and initialised as nullptr in the PluginProcessor.h file and the memory is later allocated in the PluginProcessor.cpp file in the definition of the prepareToPlay() method using the keyword new. The memory is then freed in the ~PluginProcessor (destructor) when the object passes out of scope (Savitch, 2013).
7 In the MATLAB prototype, the beatsPerMinute needed to calculate the tempo-synced delay time is set manually. In the VST3 plugin, we want to gather the bpm information directly from the settings in our DAW. This involves a call to the juce::AudioProcessor::getPlayHead() function which returns a pointer to an AudioPlayHead object. There is some logic that says if (auto* PlayHead = getPlayHead()) i.e. if PlayHead successfully returns a pointer to the current AudioPlayHead object, as opposed to a nullptr (which will be returned in the case that the host cannot provided any playHead info), then, declare an instance of AudioPlayHead::CurrentPositionInfo named newTime. This CurrentPositionInfo is a struct whose attributes are populated by the AudioPlayHead::getCurrentPosition() method. A nested if statement then checks if (PlayHead->getCurrentPosition (newTime)) i.e. check if newTime is the struct at which PlayHead is pointing. The newTime struct includes attributes:Β§ Β§ Β§ Β§
bpm timeInSamples timeInSeconds frameRate
These attributes are tied to the current host (DAW) and associated play head position and therefore newTime.bpm will give the current bpm setting within the DAW. This bpm information can then be used to calculate the delay in samples required for different note lengths using the same calculations presented in the MATLAB prototype. It is important to specify here that Presonusβ Studio One (the DAW used for testing during this project) does not have tempo recognition functionality, in that, when you load external audio files into the DAW, the newTime.bpm member variable will not pick up on the tempo of that specific audio file but rather the overall bpm setting of the DAW. The GUI allows the user to specify which of the four available note lengths they want the tempo-synced delay to adhere to. This is implemented via a juce::Button. Each time the user clicks the button the delay time will switch to a different tempo-synced delay length based on bpm and note length. The main logic to achieve this is performed via a switch statement in the pluginProcesser processBlock(). Each time the button is clicked, mNoteVal is assigned 1, 2, 3, or 4 depending on its current value. When the plugin is loaded, mNoteVal is initialised to 1 which corresponds to a 1/8th note delay time.
In order to associate the click of the button with the change of mNoteVal, button colour, and button text, the PingPongDelayAudioProcessorEditor class inherits from juce::Button::Listener class and overrides the buttonClicked() virtual void function, called when the associated button is
8 clicked. When the function is defined in the PluginEditor.cpp a conditional statement checks whether the button clicked is noteButton. If this condition is met, an if-else ladder determines the current state of emun instance noteState and assigns a corresponding lambda function to the callback object button.onClick. Each lambda function calls a method associated with the current note length which changes the state of noteState, the value of mNoteVal, noteButton colour, and noteButton text accordingly.
During the conversion from MATLAB to C++, the way that the bounds checking is accomplished is altered slightly. Because C++ is a programming language that indexes from zero, bounds checking can be achieved by taking the modulo of the read or write head against the buffer size. For any value before the read/write index meets the buffer size, ππππ/π€πππ‘π % ππ’πππππ ππ§π will return the current read/write index, however, when the read/write index reaches the buffer size, ππππ/ π€πππ‘π % ππ’πππππ ππ§π will return 0, resetting the index to the beginning of the delay line and restarting the process. This is what makes the buffers circular in the C++ implementation. This could not be attained in the same way in MATLAB as indexing starts at 0.
RESULTS AND DISCUSSION The entirety of the testing is performed on the same two-channel delta function audio file, consisting of a regular delta function as the left channel and a delayed, attenuated delta function as the right channel (fig 2). The signal was created and used for testing in MATLAB before being rendered out as an audio file using audiowrite() and used as test audio for the final JUCE plugin.
9
Figure 2. Input Signal The initial and most crucial test involves determining whether the ping pong effect has been successful i.e. are the delays bouncing back and forth between the left and right channel. For initial testing, a short simulation length of 250 samples and a short delay length of 20 samples are used. The plot in (fig 3) shows the MATLAB render for initial testing and (fig 4) shows the C++ render for comparison. The C++ render shows some minor differences in sample value as the feedback decays, but this may be attributed to the rendering process within the DAW.
Figure 3. Base Case Ping Pong Delay Algorithm MATLAB Prototype (πππππ¦πΌππππππππ = 20)
10
Figure 4. Base Case Ping Pong Delay Algorithm C++ Render (πππππ¦πΌππππππππ = 20) Before implementing the tempo-synced delay options the algorithms for the MATLAB and C++/JUCE Plugin were tested with various hardcoded (non-tempo synced) delay time values based on seconds of delay with min delay time = 0.1 second and max delay time = 2 seconds. The resulting plots can be observed in (appendix I). Tests were also performed for various values of the feedback coefficient, comparing the left and right channels for both the MATLAB and C++ render. The feedback value needs to be < 1 to ensure that the output of the delay is being multiplied by a value < 1 and therefore decaying in amplitude over time (Reiss and McPherson, 2015). The resulting plots show the expected relationship between feedback value and the number of repetitions of the signal. For a larger feedback value, the impulse decays at a slower rate and therefore has many repetitions. These repetitions are theoretically infinite, though, over time, they fall below the audible range and become imperceivable (Tarr, 2019). The ping pong effect can be observed noticeably in (fig 5) for feedback = 0.9 where the distinction and alternation between the input signalβs left channel feedback and right channel feedback are evident. The same can be observed for the corresponding C++ render (fig 6).
11
Figure 5. Various Feedback Values MATLAB Prototype (Left Channel) (πππππ¦πΌππππππππ = 20)
Figure 6. Various Feedback Values C++ Render (Left Channel) (πππππ¦πΌππππππππ = 20)
12
Figure 7. Various Feedback Values MATLAB Prototype (Right Channel) (πππππ¦πΌππππππππ = 20)
Figure 8. Various Feedback Values C++ Render (Left Channel) (πππππ¦πΌππππππππ = 20) The following plots involve the testing of the tempo-synced delay time implemented in the final JUCE plugin. For this testing phase, the expected delay times based on notelength and beatsPerMinute were first calculated in MATLAB, before testing whether the C++ render was producing the same results. %Tempo-Sync beatsPerMinute = 100; %use 100bpm for all tests beatsPerSecond = beatsPerMinute/60; %find seconds per beat by taking the inverse secondsPerBeat = 1/beatsPerSecond;
13 noteLength = 1; % test for 0.5 = 1/8th, 1 = 1/4, 2 = 1/2 delayInSamples = fix(noteLength * secondsPerBeat * Fs); %for 100bpm 1/8th note delay = 14400 samples 1/4 note delay = 28800 samples 1/2 note delay = 57600 samples
The C++ render plots demonstrate both the first sample of delay (fig 9) and the resulting feedback (appendix II) (the following tests were performed with a fixed feedback value of 0.5). Here, the initial sample of delay is of the most importance as the tests aim to determine whether the same delay length calculated in MATLAB is being produced in the C++ renders. It can be seen in the three C++ renders for initial delay length (fig 9, 10, 11) that the initial delay lengths are equivalent to those calculated in MATLAB. The
Figure 9. 1/8th Note Delay 100bpm C++ Render (First Sample Only) (πππππ¦πΌππππππππ = 14400)
14
Figure 10. 1/4 Note Delay 100bpm C++ Render (First Sample Only) (πππππ¦πΌππππππππ = 28800)
Figure 11. 1/2 Note Delay 100bpm C++ Render (First Sample Only) (πππππ¦πΌππππππππ = 57600) These final plots demonstrate a comparison for 1/8th, 1/4, and 1/2 note delay lengths for left (fig 12) and right (fig 13) channels when beatsPerMinute = 100.
15
Figure 12. 1/8, 1/4, and 1/2 Note Delay 100bpm C++ Render (Left Channel)
Figure 13. 1/8, 1/4, and 1/2 Note Delay 100bpm C++ Render (Right Channel) CONCLUSION This paper has documented the prototyping, implementation and testing of a ping pong delay VST3 plugin written in JUCE/C++. This project has introduced the theory of memory-using audio effects and use of delay lines to store previous values of an audio signal.
Limitations of tempo sync in terms of time signature
16 The modification to the base case ping pong delay algorithm saw the implementation of rhythmic delay lengths based on the input bpm and user defined note length. It is important to clarify here that this implementation is limited to 4/4-time signatures (Tarr, 2019) and has the potential to be extended in further work to support multiple time signatures. Bad access error when running in standalone The final C++/JUCE plugin builds successfully without errors as a VST3 plugin deployed with Studio One DAW, however, there have been complications building in standalone. The build is successful until a string related (char*) BAD ACCESS error occurs.
The origin of this error has been ultimately unclear, though it might suggest that a JUCE object that uses strings had passed out of scope and is being called incorrectly. Future work To make the delay effect more interesting and customizable further development could include modulation of the delay time, more specifically, modulation of rate, depth and shape (Case, 2007). This could look like: β’
Rate β To give a user control over how fast or slow the delay time changes over time
Figure 14. Modulation Rate for Delay time as a Function of Time (Case, 2007) β’
Depth β To allow a user to specify the amount in which the delay time is modulated
17
Figure 15. Modulation Depth for Delay time as a Function of Time (Case, 2007) β’
Shape β Allows the user to choose the shape of the delay time modulation (i.e. sine, square, randomised)
Figure 16. Modulation Shape for Delay time as a Function of Time (Case, 2007) These three parameters combined add another layer of user-controllable creative expression to the ping pong delay effect.
18
REFERENCES Case, A. (2007) Sound FX. 1st edn. Oxford: Focal Press, pp. 210-214. Pirkle, W. (2019) Designing Audio Effect Plugins in C++. 2nd edn. New York: Routledge, pp. 40141 Reiss, J. and McPherson, A. (2015) Audio effects: theory, implementation and application. 1st edn. Hoboken: CRC Press, pp. 24-26. Savitch, W. (2013) Absolute C++. 5th edn. Essex: Pearson Education, p. 487. Tarr, E. (2019) Hack audio. London: Routledge, Taylor & Francis Group, pp. 190-194.
19
APPENDIX I.
Delay Length 0.1 β πΉπ C++ Render
Delay Length 0.5 β πΉπ C++ Render
20
Delay Length 1.0 β πΉπ C++ Render
Delay Length 1.5 β πΉπ C++ Render
21
Delay Length 2.0 β πΉπ C++ Render II.
1/8th Note Delay 100bpm C++ Render (All Samples) (πππππ¦πΌππππππππ = 14400)
22
1/4 Note Delay 100bpm C++ Render (All Samples) (πππππ¦πΌππππππππ = 28800)
1/2 Note Delay 100bpm C++ Render (All Samples) (πππππ¦πΌππππππππ = 57600)