Tuesday 18 December 2018

Modulated Sine Waves Two Ways - a Recipe in Two Parts (Part 1)

I was one of the helpers at the recent Rebel Technology 'Gen' Workshop in association with the Music Hackspace at Somerset House in London, and I got inspired by the way that many of the attendees went beyond the basics and took a basic oscillator and modulated it with another, and modulated that, and...

So I thought that I should try to make a multi-oscillator generic version of this 'modulated oscillator' genre, and at the time I was using Max 7.3.5 and the Live-bundled Max 8.0.0 - the significance of this will become apparent later... As my previous 'BankOSC' device (and quite a few others, on various platforms!) reveal, I have a weakness for sound generators that make atmospheric drones, hums, buzzes and other background 'subliminal' noises - the sort of thing that makes the bridge of the star-ship Enterprise immediately recognisable (or the transporter noise, or the sounds or planets, or... Hmmm, time to activate the 'Star Trek' filter.)

Which begs the question, what is the underlying 'genotype' of modulated oscillators? Considering what I saw at the workshop, plus more years than I care to think about of experience, I eventually decided that it was something to do with themes like: detune, tremolo/'ring modulation' (AM) and vibrato/'frequency modulation' (FM). Now as with slash fiction (don't look it up if you don't know about it), it is the juxtaposition that matters, and here it is the broad range that tremolo (usually interpreted as meaning low frequency (10 Hz or less) amplitude modulation) and ring modulation (usually interpreted as audio frequency (20 Hz to 20 kHz) amplitude modulation) imply that is the key, and this applies to FM as well. So wide range was essential, which mean using 'exponential' scalings to keep a huge range manageable with a single rotary control.


This is a very broad specification, so I cut it down to something simpler for my first attempt, mainly because there are limits to what you can do on platforms that use gen, like the Rebel Technology devices (OWL Pedal, OWL Modular, Alchemist, Wizard...) Now, I've always preferred FM to AM because it seems to provide a broader range of timbres, and so I went for FM, leaving AM for another day. I also restricted the waveforms to sine wave, because one of the major constraints is the number of controls. (4 rotary controls, one expression pedal input and one switch on the OWL pedal, for example) I also like auto-pan, and so I subverted the AM into simple panning just to give movement to the output. Here's the gen code and the block diagram for what I made as my underlying oscillator module:

So there are two oscillators, one detuned by 1.001 (I didn't spend any time optimising this!) and a straight-forward LFO-driven anti-phase panning circuit. Above this, there is the modulation section, which has another LFO (that can also run at audio frequencies...) and uses a '+' add object to do the modulation. This gives 'linear' 'vibrato-style' FM and so I chose a suitable frequency range of 0.01 to 2.5 Hz (look in the Param C 'scale' object) - if you replace the add with a '*' 'multiply' object (and tweak the scaling multiply and set the LFO to run at audio frequencies) then you get the 'multiply' FM (frequency modulation) variant. 'Linear/vibrato' gives a milder sound, whilst 'Multiply/FM' is much brighter in timbre.


 Here's where the oscillator module fits into the main gen code. At the top there are interfaces to the rotary controls etc., followed by slide objects to smooth any bad connections in the potentiometers, and then scale objects to turn the 0 to 1 range into useful ranges for the oscillator module. The 'Spreader' gen object is my attempt to turn those single values into a range of values - there's no point sending the same control values to all of the oscillator modules, because they will then all make the same sound. So what the spreader does is turn one incoming value into several different output values, and the range of those output values is set by the 'Spread' variable.

When I wrote the spreader object, then I was using Max 7.3.5 and the bundled Max 8.0.0 that comes with Live 10. I didn't have Max 8.0.2, and so I hadn't seen the 'spread' object that is included! However, that spread object is in Max, and not in gen, and so it won't work for projects that can only use gen, as is the case with devices like those from Rebel technology.

Having to work exclusively in gen also has other consequences. One type of object that is often used in Max projects is the toggle - either the 'box with a cross', or a text box that can be used to set values to on or off. In Max this is fine, but in gen there's no such object, and what you usually get is just a push-button that goes from 0 to 1 when you press it, and from 1 to 0 when you release it. So the button has a 'momentary' action, and you can use it as a toggle that stays in one of two states, and when you press the button is goes to the other state. In order to implement this, then you need to store information about the current state in an object, and then to change that state when the button is pressed. Because this isn't needed in Max (there are already objects that provide 'toggle' outputs), then there aren't any ready-made objects in gen that solve this. So what is needed is something that turns pushes on a switch into toggling between two values.

Toggling

This sort of functionality is called sequential logic, and one of the standard ways to store a specific state is to have some feedback around a logic gate. Here's an example:


Now you don't need to know that these are NOR gates, or have any specific knowledge of how sequential logic works, because how this works is really straight-forward if you just follow what happens when we change the value of the 'Set' input from 0 (zero: false) to 1 (true). Those two gate symbols do only one thing: if either of the inputs is true, the output goes false. You can think of each NOR gate as being a bit like a person who is watching a room via closed circuit television. If they have been told to say when the room is empty, then as soon as one or more people enter the room, the room is no longer empty. So when the Set goes to 1 (true), then the output of the first gate will go to 0 (false). But this output is connected to another NOR gate, and so because Reset is also 0 (false), then this gate will output 1 (true: the room is empty!), and so the value of 'Output' will be true (1). But that true (1) value is fed back into the input of the first gate, and so that input will be 1 (true). So now there is someone in the room, which means that it isn't empty. And no matter what value 'Set' has, there's still someone in the room, and so it stays false (0), which means that the 'Output' stays at 1 (true). In order to change things, then we need to change the 'Reset' input from 0 to 1 (false to true), and then the second gate will no longer have two false at its inputs, and so will go false (the room is no longer empty!). Once the 'Output' goes false, then this goes back via the feedback to the input of the first gate, and so that now has two false, and so the output stays at true (1) (the room IS empty).

In other words, if Set goes from 0 to 1, then the value of Output goes to 1 and stays there, and after that, Set can return back to 0 because that won't affect the Output. The only way to affect the Output is to have the Reset go from 0 to 1, because that will then force the circuit to change the Output to 0 and stay there. 

So, remarkably, this simple pair of gates acts like a memory that can be set and reset. Setting and resetting a value sounds like what a toggle switch does - it sets something, then resets it, and so the state changes every time that we do a set or reset.

So to implement a toggle switch, all we need is a bit of feedback, and some way of using just one 'Set' input to do the setting and resetting, instead of two separate inputs. In gen, here's an example piece of code that does exactly that:


The input is from the push button, and goes from 0 to 1 when it is pressed, and then back to 0 again when it is released. This is turned into a quick pulse by an edge detector circuit, which is used to trigger a latch that holds the value. But we need to have the toggle action, and so we can't just put the input value in, we need to alternate between the input and its inverse. So the output of the store is delayed by the 'Previous' time delay box (1 sample delay), which then selects the inverse of the input, then the input, and so the value that is latched changes back and forth between 1 and 0 for each press of the push button. So this gen code provides exactly the 'toggle' action that is needed. 



Looking again at the main gen code, the toggle code is inside the 'gen @title toggle' object, and when the push-button is pressed, the output of the toggle object output goes to 1 and stays there, then when the push-button is pressed again, the toggle object output goes back to 0 and stays there, and so on.


The output of the toggle is multiplied by 0.4, and this is then smoothed into an envelope by the 'slide' object, which then controls the fade in and out of the volume at the outputs. 


At the very top level for the project, you can see the test controls that emulate the controls in the Rebel Technology devices: in this case an OWL pedal with four rotary controls, and Expression Pedal and a Push-button. All of the gen code from earlier is inside the gen~ object. You can also see the extra Max objects that I used whilst debugging the gen code during development - gen doesn't allow the same sort of monitoring of value directly, and so you need to use extra out<n> ports to send values up to this project level where they can be displayed. 

Upload and Download


The final result was compiled into C++ by the gen compiler inside Max, and the result uploaded to the Patches area on the Rebel technology web-site, from where it can be downloaded and put into any of the compatible devices. You may notice that there are two different versions: one for the 'Linear/Vibrato' and one for the 'Multiply/FM' versions of FM. To keep things simple, I have named these based on how they sound, not strictly according to how they work (they are both really FM!). Why two different versions? Well, there wasn't an easy way of switching between the two variations of FM because I had already used the push-button to fade the volume up and down. Of course, there's nothing to stop you altering the code to suit your own purposes...

In Part 2 (coming soon), I will take the gen code developed here, and make it into the core of a MaxForLive device...

Thanks

Thanks to everyone at Rebel Technology and the Somerset House Music Hackspace for a wonderful day!








No comments:

Post a Comment