Skip to content

Making a new module

gleb812 edited this page May 1, 2018 · 22 revisions

Making a module

This is a work in progress, the article is not finished yet.

Note (2018-03-04, issue #7): We have recently added the ability to parse module modes, which is not reflected in this tutorial yet. The instrument code will differ a little: it will include a column with mode values after parameters but before inlets. UDO developers should treat these values as typical integer arguments.

Also, option -m can help you a lot in the module implementations. [Example].

The pch2csd tool parses the Nord Modular G2 binary patch file with .pch2 extension and converts it into the .csd file of Csound. G2's modules are represented as user-defined opcodes, or UDOs for short. And although the pch2csd compiler is generally working, the tool's ability to convert your patches depends on the opcodes available. There are more than 200 modules in NMG2 and not all of them are implemented by this project at the moment. Another aspect is how close the UDOs simulate the Nord modules. Some of them can be just rough approximations made only to pass compilation, others can be more focused on the original sound. In this tutorial we briefly explain how to write your own UDO, compatible with the converter.

How the tool works

The tool uses Csound in a rather unconventional way. Typically people define variables to hold sound data and write instructions to generate or process it (1):

instr 1
  kEnv madsr iAtt, iDec, iSus, iRel 
  aVco vco2 iAmp, iFreq
  aLp moogladder aVco, iCutoff*kEnv, iRes
  out aLp*kEnv
endin

We see how envelope and waveforms are passed to the variables kEnv and aVco, then the former is used to control the cutoff of the moogladder.
The filter outputs variable aLp, which is then enveloped and sent to the output by passing the signal to the opcode out. Our tool is built in a such way that it doesn't require any variable to be connected to our UDOs. Instead, it uses the zak patching system to route signals between them. The instrument generated by the converter may look like this:

instr 1
  MySaw iFreq, iAmp 1
  MyFilter iCutoff, 1, 2
  MyOut 2
endin

MySaw, MyFilter and MyOut are the custom user-defined opcodes. The named parameters represent static values we set in the interface. The value of 1 in the MySaw opcode tells it to route the oscillator output to the 1st bus of the zak-space. The MyFilter then reads the value from this bus and passes the filtered signal to the bus 2. Finally, the MyOut UDO listens to the bus 2 and outputs the filtered signal to the system out.

Note, that the UDOs don't output anything, they just accept parameters and number of zak-space busses, all processing happens inside the opcodes. This approach separates the implementation and the composition of sound processing blocks. Basically, the purpose of the pch2csd is to connect the (user) provided user-defined opcodes together in the zak-space.

Settings things up for the UDO development

To make new modules you need to access the source code. You can either download it directly from the GitHub's web interface or use git from a terminal. The UDOs are located in the directory pch2csd/resources/modules. The file name consists of number representing the type ID of the Nord module and the .txt extension, e.g. 194.txt. Assuming you have Python 3.5 and the project dependencies installed, you can run the tool directly from the sources by invoking the pch2csd/app.py script, like this:

python3 pch2csd/app.py -h

Implementing LevAmp

Exploring the module

For the sake of simplicity we will implement a trivial module, the level amplifier, or LevAmp:

LevAmp module

This module has two parameters, an amplification knob and linear / logarithmic scale switch, one input and one output. The module multiplies the input signal by the value set by the knob. The blue color of the input and output pins means that the module works with control rate signals. But this module, as many other ones in G2, can be polymorphous, i.e. can change the pin types, from the control to the audio rate (blue to red) depending on the type of connected cables:

Nord Modular polymorphism

Other examples of such modules include, for example, EnvADSR and all mixers:

Nord Modular polymorphism

Note, that all pins in the mixer change their colors, but only two are changed in the envelope. Last but not least, when connected to non-convertable pin, the rate of the signal passing through a cable is converted on the fly. This conversion is handled by the pch2csd tool automatically.

Writing a template

First we make a simple patch consisting of a single LevAmp module and checking its type id:

$ pch2csd -p LevAmp.pch2
...
Name      ID    Type  Parameters    Area
------  ----  ------  ------------  ------
LevAmp     1      81  [64, 0]       VOICE
...

So, the LevAmp integer type is 81. This module makes it simple to understand, that the first parameter is a knob, and the second is the button. Other modules may experimentation to understand the mappings between parameters and the knobs.

Now let's check is there is already an UDO template for this module:

$ pch2csd --check-template 81
Checking UDO for the type ID 81
Found module of this type: LevAmp
ERROR UdoTemplate(LevAmp, 81.txt): no opcode 'args' annotations were found in the template

The error message tells us that the .txt file with the UDO exists, but it hasn't been properly implemented yet. This message in particular informs us that no special opcode annotations were found. The args annotations declare the number of UI parameters, as well as the number and types (audio or control rates) of inlets and outlets (ports for connecting virtual cables).

We have many unfinished templates in the pch2csd/resources/modules directory to get you started quite quickly. You can put your new module into the user directory ~/pch2csd/modules. When looking up the module template— the converter first checks the local directory, and if it didn't find the template there, it falls back to the default directory. This makes it convenient to test new module implementation without running local version of the tool.

The UDO for the module LevAmp module can look like this:

;@ map LVLamp CLAEXP 
;@ map BUT002

;@ ins k
;@ outs k
opcode LevAmp,0,kkkk
  kValue, kScale, kzIn, kzOut xin
  kIn zkr kzIn
  if kScale != 1 goto LinScale
    kMult table kValue, giCLAEXP
    goto Out
  LinScale:
    kMult table kValue, giLVLamp
  Out:
  zkw kIn * kMult, kzOut
endop

;@ ins a
;@ outs a
opcode LevAmp,0,kkkk
  kValue, kScale, kzIn, kzOut xin
  aIn zar kzIn
  if kScale != 1 goto LinScale
    kMult table kValue, giCLAEXP
    goto Out
  LinScale:
    kMult table kValue, giLVLamp
  Out:
  zkw aIn * kMult, kzOut
endop

We introduced several special annotations to help the tool in the conversion process. They are all put in the comments like this: `;@ <...>', and currently can be of the following types:

  • ;@ ins <list of input types> ;@ outs <list of out types> — these annotations should be added just before the [opcode] definition. Their function is to declare the number of input and output buses, as well the buses' signal types. They are needed for the tool to properly establish connections between the modules in the zak-space, as well as to properly convert between cable types and to choose a proper UDO variant in the case of polymorphous modules. In the example above, the ins annotation of the default UDO variant declare one input as k, and also one output of the same type. The two UDO variants differ by the types of inputs and outputs: in the first UDO inputs and outputs are of the k-rate type, and in the second UDO they are of the a-rate type. The converter chooses only one UDO for the module depending on what types of cables are coming into the module.
  • ;@ map <args>. The NM stores module parameter values as a 7-bit MIDI values (0..127), which need to be mapped to UDO dependent values — like the attack time in milliseconds or the compressor threshold in dB. We copied (most of) all tables and put them in the value_maps.json file, which is generated from the value_maps.ods LibreOffice spreadsheet. If you notice that some table is missing, then you can manually add it there. Check the value_maps.ods spreadsheet in the docs directory to check the available mapping tables.

Testing the UDO

Once you have created your UDO template and saved it as a file 81.txt in the ~/pch2csd/modules directory, you can run the pch2csd tool from the command line to convert the patch with the new UDO:

pch2csd LevAmp.pch2

This action will create the Csound source file LevAmp.pch2.csd in the same directory:

<CsoundSynthesizer>
<CsOptions>
</CsOptions>
<CsInstruments>
sr = 96000
kr = 24000
nchnls = 2
0dbfs = 1

zakinit 2, 2

; Function tables for rectifier 
giHalfPos ftgen 10, 0, 32768, 7, 0, 16384, 0, 16384, 1
giHalfNeg ftgen 11, 0, 32768, 7, -1, 16384, 0
giFullPos ftgen 12, 0, 32768, 7, 1, 16384, 0, 16384, 1
giFullNeg ftgen 13, 0, 32768, 7, -1, 16384, 0, 16384, -1
;---------------------------------
; Function tables for sequencer
giSeqTab1 ftgen 20, 0, 16, -2, 1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0 
giSeqTab2 ftgen 20, 0, 16, -2, 1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0 
;----------------------------------
; Function table generation block
giSin ftgen 1, 0, 16384, 10, 1
giTri ftgen 2, 0, 16384, 7, 0, 4096, 1, 8192, -1, 4096, 0
giSaw ftgen 3, 0, 16384, 7, 1, 16384, -1
giSqr50 ftgen 4, 0, 16384, 7, 1, 8192, 1, 0, -1
giSqr25 ftgen 5, 0, 16384, 7, 1, 4096, 1, 0, -0.25
giSqr10 ftgen 6, 0, 16384, 7, 1, 1024, 1, 0, -0.0625
;---------------------------------
giCLAEXP ftgen 100, 0, 128, 2, 0.0, 0.008, 0.016, 0.023, 0.031, 0.039, 0.047, 0.055, 0.063, 0.07, 0.078, 0.086, 0.094, 0.102, 0.109, 0.117, 0.125, 0.133, 0.141, 0.148, 0.156, 0.164, 0.172, 0.18, 0.188, 0.195, 0.203, 0.211, 0.219, 0.227, 0.234, 0.242, 0.25, 0.258, 0.266, 0.273, 0.281, 0.289, 0.297, 0.305, 0.313, 0.32, 0.328, 0.336, 0.344, 0.352, 0.359, 0.367, 0.375, 0.383, 0.391, 0.398, 0.406, 0.414, 0.422, 0.43, 0.438, 0.445, 0.453, 0.461, 0.469, 0.477, 0.484, 0.492, 0.5, 0.508, 0.516, 0.523, 0.531, 0.539, 0.547, 0.555, 0.563, 0.57, 0.578, 0.586, 0.594, 0.602, 0.609, 0.617, 0.625, 0.633, 0.641, 0.648, 0.656, 0.664, 0.672, 0.68, 0.688, 0.695, 0.703, 0.711, 0.719, 0.727, 0.734, 0.742, 0.75, 0.758, 0.766, 0.773, 0.781, 0.789, 0.797, 0.805, 0.813, 0.82, 0.828, 0.836, 0.844, 0.852, 0.859, 0.867, 0.875, 0.883, 0.891, 0.898, 0.906, 0.914, 0.922, 0.93, 0.938, 0.945, 0.953, 0.961, 0.969, 0.977, 0.984, 1.0

giLVLamp ftgen 100, 0, 128, 2, 0.0, 0.008, 0.016, 0.023, 0.031, 0.039, 0.047, 0.055, 0.063, 0.07, 0.078, 0.086, 0.094, 0.102, 0.109, 0.117, 0.125, 0.133, 0.141, 0.148, 0.156, 0.164, 0.172, 0.18, 0.188, 0.195, 0.203, 0.211, 0.219, 0.227, 0.234, 0.242, 0.25, 0.258, 0.266, 0.273, 0.281, 0.289, 0.297, 0.305, 0.313, 0.32, 0.328, 0.336, 0.344, 0.352, 0.359, 0.367, 0.375, 0.383, 0.391, 0.398, 0.406, 0.414, 0.422, 0.43, 0.438, 0.445, 0.453, 0.461, 0.469, 0.477, 0.484, 0.492, 0.5, 0.508, 0.516, 0.523, 0.531, 0.539, 0.547, 0.555, 0.563, 0.57, 0.578, 0.586, 0.594, 0.602, 0.609, 0.617, 0.625, 0.633, 0.641, 0.648, 0.656, 0.664, 0.672, 0.68, 0.688, 0.695, 0.703, 0.711, 0.719, 0.727, 0.734, 0.742, 0.75, 0.758, 0.766, 0.773, 0.781, 0.789, 0.797, 0.805, 0.813, 0.82, 0.828, 0.836, 0.844, 0.852, 0.859, 0.867, 0.875, 0.883, 0.891, 0.898, 0.906, 0.914, 0.922, 0.93, 0.938, 0.945, 0.953, 0.961, 0.969, 0.977, 0.984, 1.0

opcode LevAmp,0,kkkk
  kValue, kScale, kzIn, kzOut xin
  kIn zkr kzIn
  if kScale != 1 goto LinScale
    kMult table kValue, giCLAEXP
    goto Out
  LinScale:
    kMult table kValue, giLVLamp
  Out:
  zkw kIn * kMult, kzOut
endop

; --------------------
; VOICE AREA
instr 1
; Module    Parameters    Inlets      Outlets
LevAmp_v0   64,0,        1,                0
endin

; --------------------
; FX AREA
instr 2
; Module    Parameters    Inlets    Outlets
endin

</CsInstruments>
<CsScore>
i1 0 [60*60*24*7]
i2 0 [60*60*24*7]
</CsScore>
</CsoundSynthesizer>

Please note, that by default inlet of LevAmp_v0 opcode is connected to the bus 1 and the outlet to the bus 0 in the zak-space. These are the special buses: bus 0 is used as the “trash” bus, where we simply send audio and never read from it, and bus 1 is a bus which contains only zeros. By default, all unused outlets are passing signals to the bus 0, and all unused inlets are receiving zero signal from the bus 1. The _v0 suffix of the opcode name means that the first UDO version, the k-rate one, was used.

To run this file in Csound, you can use the command line interface or, for example, CsoundQt to make things even more simplier.

Contributing

The converter code can be considered to be stable, but the tool itself lacks of many essential modules implemented as UDOs. We are slowly implementing them, but if you wish to help, it will be greatly appreciated. The list of modules and their implementations status can be found on the “Module implementation status” wiki page. You can submit your implementations either through pull requests to the main repo or just file up the issue with your code if you're not proficient with Git, and we will upload it ourselves. The code contribution are warmly appreciated as well!