Playback

The play method

Each MObj (Note, Chord, Chain, etc.) can play itself by calling the play() method.

Audio playback and offline rendering within maelzel is delegated to csound (https://csound.com/) using csoundengine. This makes it possible to interact between the core classes, like Note or Voice and other “unrelated” parts of maelzel, like the maelzel.snd.audiosample.Sample class.

Each MObj expresses its playback in terms of a list of SynthEvent.

synthesis events

A SynthEvent is a score line with a number of fixed fields, user-defined fields and a sequence of breakpoints to define pitch, amplitude and any other parameter over time.

Within the play() method a number of parameters regarding playback can be determined. **Such parameters are common to all objects. **The most relevant of these playback parameters are:

instr

which instrument preset (see PresetDef for more information) is used for playback. If not given the default preset is used, as determined in the configuration (see play.instr)

delay

delay in seconds, added to the start of the object As opposed to the offset attribute of each object, which is defined in quarternote beats, the delay is always a time in seconds

gain

modifies the own amplitude for playback/recording (0-1)

chan

the channel to output to. Channels start at 1

pitchinterpol

‘linear’, ‘cos’, ‘freqlinear’, ‘freqcos’

fade

fade duration in seconds, can be a tuple (fadein, fadeout)

position

the panning position (0=left, 1=right)

args

a Preset can define custom parameters, like a cutoff frequency for a filter or a modulation ratio for FM synthesis, etc.

Presets - Introduction

maelzel uses csound for audio synthesis in realtime and offline. Within an instrument preset (a PresetDef) the user is given three variables: kfreq (the current frequency of the event), kamp (the current amplitude of the event) and kpitch (the current midinote of the event, corresponds to kfreq). With this information the user needs to provide the audio-generating part using csound code, by assigning the audio output to the variable aout1 for channel 1, aout2 for channel 2, etc.

There are a number of built-in Presets, but it is very easy to define new Presets. A new Preset is defined via defPreset() or defPresetSoundfont() (a shortcut to define a preset by just pointing to a soundfont).

Example

from maelzel.core import *
f0 = Note("1E")
notes = Chain([Note(f2m(f0.freq*i), dur=0.5) for i in range(20)])
defPreset("detuned", r'''
    ; kfreq and kamp are always available within the preset body
    a0 = vco2(kamp/3, kfreq)
    a1 = vco2(kamp/3, kfreq+2)
    a2 = vco2(kamp/3, kfreq-3)
    ; aout1 is assigned the audio output (this is a mono-preset)
    aout1 = sum(a0, a1, a2)
''')
notes.play(instr='detuned')

Playback attributes (setPlay)

Any playback attribute determined via the play() method can be set beforehand for each object individually, using setPlay(). Then, when this object is played any play settings fixed via .setPlay is used as if it was passed to play().

In the following example we fix the instr of 1 every 4 notes to ‘mypiano’. For the rest of the notes this settings stays undetermined. When .play is called on the Chain, 'saw' (which is a built-in preset) is set as the default instr. Any fixed playarg will be used for playback, otherwise a group default is used (the instr set for Chain) or, as a fallback, the default instr as defined in the config (key: ‘play.instr’).

.play param → param set via .setPlay → .play param for the group → default value

Example

from maelzel.core import *
defPresetSoundfont('mypiano', '/path/to/mypiano.sf2')
notes = Chain([Note(m, dur=1) for m in range(48, 72)])
for i, note in enumerate(notes):
    if i%4 == 0:
        note.setPlay(instr='mypiano')
notes.play(instr='saw')

Preset parameters

A Preset can have parameters which modify its behaviour. For example, an FM-synth preset might allow to define modulation ratio or depth, or a subtractive-synth preset could define parameters for its filter. Any custom parameter must define a default value

from maelzel.core import *
defPreset('substractive', r'''
    a0 vco2 kamp, kfreq, 10
    aout1 moogladder a0, lag:k(kcutoff, 0.1), kq
''', params={'kcutoff': 3000, 'kQ': 0.9})
Chord("C4 E4 G4", dur=10).play(instr='substractive', args={'kcutoff': 1000})

Preset parameters can also be defined inline, following the syntax for csoundengine’s Instr

from maelzel.core import *
defPreset('substractive', r'''
    |kcutoff=3000, kQ=0.9|
    a0 vco2 kamp, kfreq, 10
    aout1 moogladder a0, lag:k(kcutoff, 0.1), kq
''')
Chord("C4 E4 G4", dur=10).play(instr='substractive', kcutoff=1000)

Recording / Offline Rendering

By replacing a call to play() with a call to rec() it is possible to render any MObj as a soundfile (offline rendering). To render multiple objects to the same soundfile you can either group them in a container (for example, place multiple notes inside a Chain and render that) or render them via render

from maelzel.core import *
voice1 = Voice([Note(m, dur=1)
                for m in range(60, 72)])
voice2 = Voice([Note(m+0.5, dur=1.75)
                for m in range(60, 72)])
voice1.setPlay(instr='saw')
voice2.setPlay(instr='tri')
render("out.wav", [voice1, voice2])

Notice that in this way, any parameter normally passed to .rec needs to be fixed via .setPlay.

Using render as context manager

A simpler method is to use the render() as context manager:

from maelzel.core import *
voice1 = Voice([Note(m, dur=1)
                for m in range(60, 72)])
voice2 = Voice([Note(m+0.5, dur=1.75)
                for m in range(60, 72)])
with render('out.wav'):
    voice1.play(instr='saw')
    voice2.play(instr='tri')

Within the context manager the same code used for playing in realtime can be used to render offline (see also: OfflineRenderer)


Playback API