The Csound Blog by Jacob Joaquin email jacobjoaquin@gmail.com web www.thumbuki.com/csound/blog (C)2007 Jacob Joaquin Licensed under Creative Commons (see below) 2007.06.20 Modular Instruments When designing a virtual instrument in csound, the easiest approach is to create a single self-contained instr. Within this instr, any number of opcodes can be arranged in a near-infinite number of combinations. Each instance of the instr manages its own local memory space/variables/signals. Users customize the interface of the instr by utilizing p-fields. With this fundamental model of instrument design, it is already apparent that Csound excels in terms of modularity. However, there are many other approaches that expand this concept even further. Since around the time I started The Csound Blog, I've been experimenting heavily with small networks of modularized instrs that can be combined to make one super instrument, not too unlike Voltron.[1] One existing blog example is "Adding Zak to the Mix," where the parameters of the fx instruments are modified by control instruments.[2] Not to mention the hundreds of other examples on the net that have been around for years. After much trial and error, I've quantified my experiments into a model consisting of three elements: Synth Engine, Memory and Interface. For convenience, I will refer to this model as the SEMI system for the remainder of this blog. At the center of a SEMI system is the memory core. The memory may consist of global variables, zak bus, f-tables, or anything else that stores data outside of the synth engine, yet is accessible by both the synth engine and interface. The user interacts with a SEMI system instrument through the interface. The interface may be a single or series of user-defined instrs. Interface instrs are designed to modify the data stored in the memory. The synth engine then gets its parameter settings by reading directly from the memory, and thus modifying the behavior of the synth engine. There will be times when it is prudent for the interface to read from the memory and the engine to write to the memory. Though the interface and engine rarely, if ever, need to communicate directly with each other. .------------. .------------. .------------. | Interface |<--->| Memory |<--->| Engine | '------------' '------------' '------------' Figure 1. A diagram of the SEMI system model. (requires fixed width font to view properly) Here are a few things that can be done with the SEMI system. One issue with p-fields is that they are static in relation to any given instance of an instrument. Once they are set, they are set for the entire duration of the note. By moving parameters into an external memory space, users can modify the state of the synth engine at any point using separate control instruments. Several synthesizers or effects boxes can be made from a single well-designed synth engine through the creation of interface instruments. For example, if you want a FLTK, MIDI or OSC version of your SEMI system instrument, you could do so by making new interface instrs, rather than making changes to the engine itself. Or you can extend the functionality by creating new specialized control instruments, like an LFO instr. Simple Instrument To demonstrate a working model of the SEMI system, I designed a crude synthesizer called Simple. The synth engine has a single oscillator, a dual mode filter and an amplitude ADSR envelope. Simple reminds me of vintage lo-rez digital synths, which I happen to fancy. I created the memory core with the zak bus system and used macros to name the parameters. Here is the list of the parameters along with short descriptions: kSimpleTranspose Pitch control, in semitones. kSimpleWaveform F-table number of the waveform. kSimpleFilterMode The mode of the filter. The filter will act as a lowpass filter when set to 0 and a highpass when set to 1. The mode is also sweepable, and acts as a notch filter when the value equals 0.5. kSimpleFilterFreq The cutoff frequency of the filter. kSimpleFilterRes The amount of resonance to apply to the lowpass filter. kSimpleAmplitude The master volume of the instrument. iSimpleAttack Attack time of the envelope. iSimpleDecay Decay time of the envelope. iSimpleSustain Sustain level of the envelope. iSimpleRelease Release time of the envelope. Simple is polyphonic, as it can play any number of notes at any given time. However, each instance of Simple share the same memory space. If kSimpleFilterFreq is set to 1000, then all active instances of Simple have a cutoff frequency of 1000Hz. The envelope related memories work a little differently than the rest. The amplitude ADSR mechanism is hardwired into the synth engine. When an instance of Simple is initialized, the four stored envelope values are read by the engine at i-time. Any changes made to these four memories will only affect future instances. For the interface, I've designed five control instruments: Parameter, Envelope, LFO, SampleAndHold and Pattern. These are designed so that they will work with any zak k-rate based memory, even with those not related to Simple. In my opinion, these are what breath life into the instrument. The most basic controller is the Parameter instrument. It accepts a p-field for which parameter to modify and a new value. It works at i-time, so after the first pass, it stops writing to the memory, making the duration, aka p3, mostly useless. The Envelope controller can be compared to that of a knob or fader. Just like the Parameter instrument, it accepts a parameter to modify, and a new value. The difference is that it will glissando from the old value to the new value over the duration specified in p3. This version of Envelope is linear. If you want an an exponential version, I recommend building one based on the expcurve opcode. The next in line is the LFO control instrument. This one is a little bit more complex, as it does a little more. It accepts arguments for which parameter, the frequency, f-table number of the waveshape, and minimum and maximum values for the range of the output. I particularly like this instr as it shows that you can move some of Csound's modular capabilities into the score. It's nice being able to start and stop an LFO at any time, independent of the synth engine. Sample and hold is a staple of modular synthesizer culture, that's why I thought it would be fun to include one as a control instrument. SampleAndHold takes the same arguments as LFO, minus waveshape. The final control instrument is Pattern. You can think of it as a mini-step sequencer or arpeggiator, as it loops through a table of discrete values. The arguments include the parameter to modify, the frequency in which the table is looped through, and the f-table that stores the values. There is one issue you should be aware of. My goal was to reduce the complexity of the example, which ended in a few trade-offs. One such trade-off is that you should only use one control instrument at a time for any individual memory/parameter. Otherwise, unexpected results will happen. Though it is possible to design a SEMI system where any number of control instruments can overlap and be mix together. I think that's all I have to say on this for now. I hope you enjoy. Until next time, Jake Permalink http://www.thumbuki.com/20070620/modular-instruments.html References [1] Voltron http://en.wikipedia.org/wiki/Voltron [2] The Csound Blog - Adding Zak to the Mix http://www.thumbuki.com/csound/files/thumbuki20070410.csd License (cc) Creative Commons Attribution-ShareAlike 2.5 You are free: * to Share -- to copy, distribute, display, and perform the work * to Remix -- to make derivative works Under the following conditions: * Attribution. You must attribute the work in the manner specified by the author or licensor. * Share Alike. If you alter, transform, or build upon this work, you may distribute the resulting work only under a license identical to this one. * For any reuse or distribution, you must make clear to others the license terms of this work. * Any of these conditions can be waived if you get permission from the copyright holder. http://creativecommons.org/licenses/by-sa/2.5/ sr = 44100 kr = 4410 ksmps = 10 nchnls = 1 zakinit 1, 10 ; ---- Zak Busses ---- ; k-rate zak channels # define kSimpleTranspose # 1 # # define kSimpleWaveform # 2 # # define kSimpleFilterMode # 3 # # define kSimpleFilterFreq # 4 # # define kSimpleFilterRes # 5 # # define kSimpleAmplitude # 6 # # define iSimpleAttack # 7 # # define iSimpleDecay # 8 # # define iSimpleSustain # 9 # # define iSimpleRelease # 10 # ; ---- Instrument Definitions ---- # define Parameter # 1 # # define Envelope # 2 # # define LFO # 3 # # define SampleAndHold # 4 # # define Pattern # 5 # # define Simple # 6 # ; ---- Tempo ---- #define tempo # 120 # instr $Parameter iparameter = p4 ivalue = p5 ziw ivalue, iparameter endin instr $Envelope idur = p3 iparameter = p4 ivalue = p5 ioldValue zir iparameter kenv line ioldValue, idur, ivalue zkw kenv, iparameter endin instr $LFO idur = p3 iparameter = p4 ifreq = p5 ishape = p6 imin = p7 imax = p8 iframe = 1 / kr * $tempo / 60 * 3 ivalue zir iparameter koscil oscil 0.5, ifreq * $tempo / 60, ishape koscil = ( koscil + 0.5 ) * ( imax - imin ) + imin kenv linseg 1, idur - iframe, 1, 0, 0, iframe, 0 kout = ( koscil + ivalue ) * kenv + ivalue * ( 1 - kenv ) zkw kout, iparameter endin instr $SampleAndHold idur = p3 iparameter = p4 ifreq = p5 imin = p6 imax = p7 iframe = 1 / kr * $tempo / 60 * 3 ivalue zir iparameter krnd randh 0.5, ifreq * $tempo / 60 krnd = ( krnd + 0.5 ) * ( imax - imin ) + imin kenv linseg 1, idur - iframe, 1, 0, 0, iframe, 0 kout = ( krnd + ivalue ) * kenv + ivalue * ( 1 - kenv ) zkw kout, iparameter endin instr $Pattern idur = p3 iparameter = p4 ifreq = p5 itable = p6 iframe = 1 / kr * $tempo / 60 * 3 ivalue zir iparameter kclock phasor 1 / idur * ifreq kpattern table kclock, itable, -1 kenv linseg 1, idur - iframe, 1, 0, 0, iframe, 0 kout = ( kpattern + ivalue ) * kenv + ivalue * ( 1 - kenv ) zkw kout, iparameter endin instr $Simple idur = p3 ipch = cpspch( p4 ) kpitch zkr $kSimpleTranspose kwaveform zkr $kSimpleWaveform kfilterMode zkr $kSimpleFilterMode kfilterFreq zkr $kSimpleFilterFreq kfilterRes zkr $kSimpleFilterRes kamplitude zkr $kSimpleAmplitude iattack zir $iSimpleAttack idecay zir $iSimpleDecay isustain zir $iSimpleSustain irelease zir $iSimpleRelease aosc oscilikt 1, ipch*2^(kpitch/12), kwaveform, -1 alp moogladder aosc, kfilterFreq, kfilterRes, 0 ahp buthp aosc, kfilterFreq afilter = alp * ( 1 - kfilterMode ) + ahp * kfilterMode avca = afilter * kamplitude kenv linsegr 0, iattack, 1, idecay, isustain, idur - iattack - idecay, isustain, irelease, 0 out avca * 0dbfs * kenv endin ;---- Zak Busses ---- ; k-rate zak channels # define kSimpleTranspose # 1 # # define kSimpleWaveform # 2 # # define kSimpleFilterMode # 3 # # define kSimpleFilterFreq # 4 # # define kSimpleFilterRes # 5 # # define kSimpleAmplitude # 6 # # define iSimpleAttack # 7 # # define iSimpleDecay # 8 # # define iSimpleSustain # 9 # # define iSimpleRelease # 10 # ;---- Instrument Definitions ---- # define Parameter # 1 # # define Envelope # 2 # # define LFO # 3 # # define SampleAndHold # 4 # # define Pattern # 5 # # define Simple # 6 # ; ---- Tempo ---- #define tempo # 120 # ; ---- f-tables ---- ; Waveforms f1 0 65537 10 1 ; Sine f2 0 65537 -7 0 16384 1 32768 -1 16386 0 ; Triangle f3 0 65537 -7 1 32768 1 0 -1 32768 -1 ; Square f4 0 65537 -7 -1 65526 1 ; Saw Up f5 0 65537 21 1 ; White Noise f10 0 65537 10 1 0 [-1/11] 0 [1/19] 0 [-1/27] 0 [1/35] ; Band-limited Triangle f11 0 65537 10 1 0 [1/3] 0 [1/5] 0 [1/7] 0 [1/9] ; Band-limited Square f12 0 65537 10 1 [1/2] [1/3] [1/4] [1/5] [1/6] [1/7] [1/8] [1/9] ; Band-limited Saw ; Note Patterns f22 0 9 -2 12 9 4 0 12 9 4 11 f23 0 9 -2 12 7 3 -2 -2 12 7 0 ; ---- Parameter Demo ---- t 0 $tempo i $Parameter 0 1 $kSimpleTranspose 0 i $Parameter 0 1 $kSimpleWaveform 1 i $Parameter 0 1 $kSimpleFilterMode 0 i $Parameter 0 1 $kSimpleFilterFreq 22050 i $Parameter 0 1 $kSimpleFilterRes 0.0 i $Parameter 0 1 $kSimpleAmplitude 0.5 i $Parameter 0 1 $iSimpleAttack 0.1 i $Parameter 0 1 $iSimpleDecay 0.1 i $Parameter 0 1 $iSimpleSustain 0.5 i $Parameter 0 1 $iSimpleRelease 0.4 i $Simple 0 16 7.00 i $Parameter 1 1 $kSimpleWaveform 10 i $Parameter 1.5 1 $kSimpleWaveform 11 i $Parameter 2 1 $kSimpleWaveform 4 i $Parameter 2.5 1 $kSimpleWaveform 5 i $Parameter 3 1 $kSimpleWaveform 10 i $Parameter 3.5 1 $kSimpleWaveform 3 i $Parameter 4 1 $kSimpleWaveform 12 i $Parameter 4.5 1 $kSimpleWaveform 5 i $Parameter 5 1 $kSimpleWaveform 2 i $Parameter 5.5 1 $kSimpleWaveform 3 i $Parameter 6 1 $kSimpleWaveform 4 i $Parameter 6.5 1 $kSimpleWaveform 5 i $Parameter 7 1 $kSimpleAmplitude 1.5 i $Parameter 7 1 $kSimpleFilterRes 0.5 i $Parameter 7 1 $kSimpleFilterFreq 4000 i $Parameter 7.5 1 $kSimpleFilterFreq 1000 i $Parameter 8 1 $kSimpleFilterFreq 7000 i $Parameter 8.5 1 $kSimpleFilterFreq 2000 i $Parameter 9 1 $kSimpleFilterFreq 4000 i $Parameter 9.5 1 $kSimpleFilterFreq 1000 i $Parameter 10 1 $kSimpleFilterFreq 7000 i $Parameter 10.5 1 $kSimpleFilterFreq 10000 i $Parameter 11 0.5 $kSimpleFilterMode 0 i $Parameter + . $kSimpleFilterMode > i $Parameter + . $kSimpleFilterMode > i $Parameter + . $kSimpleFilterMode > i $Parameter + . $kSimpleFilterMode > i $Parameter + . $kSimpleFilterMode > i $Parameter + . $kSimpleFilterMode > i $Parameter + . $kSimpleFilterMode > i $Parameter + . $kSimpleFilterMode > i $Parameter + . $kSimpleFilterMode > i $Parameter + . $kSimpleFilterMode 1 s f 0 1 s ; ---- Envelope Demo ---- t 0 $tempo i $Parameter 0 1 $kSimpleTranspose -17 i $Parameter 0 1 $kSimpleWaveform 4 i $Parameter 0 1 $kSimpleFilterMode 0 i $Parameter 0 1 $kSimpleFilterFreq 500 i $Parameter 0 1 $kSimpleFilterRes 0.5 i $Parameter 0 1 $kSimpleAmplitude 0.05 i $Parameter 0 1 $iSimpleAttack 0.08 i $Parameter 0 1 $iSimpleDecay 0.3 i $Parameter 0 1 $iSimpleSustain 0.05 i $Parameter 0 1 $iSimpleRelease 0.1 i $Envelope 0 4 $kSimpleFilterMode 0.1 i $Envelope 4 4 $kSimpleFilterMode 0.0 i $Envelope 8 4 $kSimpleFilterMode 0.1 i $Envelope 12 4 $kSimpleFilterMode 0.0 i $Envelope 0 10 $kSimpleFilterFreq 900 i $Envelope 10 7 $kSimpleFilterFreq 400 i $Envelope 0 8 $kSimpleAmplitude 0.8 i $Envelope 0 16 $iSimpleDecay 0.6 i $Simple 0 1 7.00 i $Simple 1 1 7.04 i $Simple 2 1 7.07 i $Simple 3 1 7.11 i $Simple 4 1 7.10 i $Simple 5 1 7.07 i $Simple 6 1 7.04 i $Simple 7 1 7.01 i $Simple 8 1 7.02 i $Simple 9 1 7.05 i $Simple 10 1 7.09 i $Simple 11 1 8.00 i $Simple 12 1 7.11 i $Simple 13 1 7.07 i $Simple 14 1 7.05 i $Simple 15 1 7.02 i $Simple 16 2 7.00 s f 0 1 s ; ---- LFO Demo ---- t 0 $tempo i $Parameter 0 1 $kSimpleTranspose 0 i $Parameter 0 1 $kSimpleWaveform 4 i $Parameter 0 1 $kSimpleFilterMode 0 i $Parameter 0 1 $kSimpleFilterFreq 800 i $Parameter 0 1 $kSimpleFilterRes 0.9 i $Parameter 0 1 $kSimpleAmplitude 0.5 i $Parameter 0 1 $iSimpleAttack 0.1 i $Parameter 0 1 $iSimpleDecay 0.1 i $Parameter 0 1 $iSimpleSustain 0.5 i $Parameter 0 1 $iSimpleRelease 0.1 i $LFO 0 16 $kSimpleFilterMode 0.333 2 0.2 0.8 i $Simple 0 4 8.00 i $LFO 2 2 $kSimpleTranspose 4 3 0 12 i $Simple 4 4 7.09 i $LFO 6 2 $kSimpleTranspose 4 3 0 7 i $Simple 8 4 7.07 i $LFO 10 2 $kSimpleTranspose 16 3 7 12 i $Parameter 12 1 $iSimpleRelease 0.5 i $Simple 12 4 7.00 i $LFO 14 2 $kSimpleTranspose 7 3 12 19 s f 0 1 s ; ---- SampleAndHold Demo ---- t 0 $tempo i $Parameter 0 1 $kSimpleTranspose 0 i $Parameter 0 1 $kSimpleWaveform 5 i $Parameter 0 1 $kSimpleFilterMode 0 i $Parameter 0 1 $kSimpleFilterFreq 440 i $Parameter 0 1 $kSimpleFilterRes 0.95 i $Parameter 0 1 $kSimpleAmplitude 0.5 i $Parameter 0 1 $iSimpleAttack 0.5 i $Parameter 0 1 $iSimpleDecay 0.5 i $Parameter 0 1 $iSimpleSustain 0.75 i $Parameter 0 1 $iSimpleRelease 0.0 i $Envelope 0 8 $kSimpleFilterRes 0.999 i $Envelope 8 8 $kSimpleFilterRes 0.95 i $SampleAndHold 0 16 $kSimpleFilterFreq 4 100 1000 i $Simple 0 16 7.00 s f 0 1 s ; ---- Pattern Demo ---- t 0 $tempo i $Parameter 0 1 $kSimpleTranspose 0 i $Parameter 0 1 $kSimpleWaveform 4 i $Parameter 0 1 $kSimpleFilterMode 0 i $Parameter 0 1 $kSimpleFilterFreq 1100 i $Parameter 0 1 $kSimpleFilterRes 0.125 i $Parameter 0 1 $kSimpleAmplitude 0.5 i $Parameter 0 1 $iSimpleAttack 2.0 i $Parameter 0 1 $iSimpleDecay 0.1 i $Parameter 0 1 $iSimpleSustain 1.0 i $Parameter 0 1 $iSimpleRelease 4.0 i $Envelope 0 16 $kSimpleFilterFreq 400 i $Envelope 16 16 $kSimpleFilterFreq 1100 i $Envelope 16 1 $kSimpleFilterRes 0.85 i $Pattern 0 8 $kSimpleTranspose 4 22 i $Pattern 8 8 $kSimpleTranspose 4 23 i $Pattern 16 8 $kSimpleTranspose 4 22 i $Pattern 24 8 $kSimpleTranspose 4 23 i $Simple 0 32 7.00 i $Envelope 34 2 $kSimpleFilterMode 1.0 i $Envelope 34 2 $kSimpleFilterRes 0.0 i $Envelope 32 4 $kSimpleAmplitude 0.0