Hi,
I am looking for feedback on implementing a keyboard sustain/damper pedal in Cabbage/Csound. Basically something that mimics a pedal on an acoustic piano. The instrument should listen to MIDI notes, while a MIDI mapped slider/expression pedal is used to dynamically control sustain. I imagine there might be a more elegant and efficient way of doing this compared to my solution below. I haven’t found a solution in literature nor in examples. An example of such implementation would be very useful though. I hope you can help me learn something here.
Since I want to control the sustain/damping of notes (instruments) dynamically, it seems that the standard Csound solution employing envelopes with release segments is not feasible.
My solution involves triggering instruments with indefinite durations, turning them off within the instrument instances and substituting envelope opcodes for attack and release with k-rate multiplications. I don’t need the full ADSR envelope so it was sufficient with an attack and release. A global sustain variable (gkSustain
), which can be MIDI mapped in a DAW, is used to “open” and “close” the keyboard and instruments are turned off when the volume envelope reaches a very small threshold and the key (note) is released.
When hitting a note repeatedly, I have experienced clicks, and I guess this is not a problem specific to my implementation? It happens because the instrument and its volume envelope get re-triggered. To resolve this, I’m not re-triggering any instruments/notes that are still playing but only re-trigger the envelope, however, not starting from zero but from the current value (in release phase). To enable this, I have used a rather brute force approach and keep track of volume envelopes for all notes using a table (gkNoteVolume[]
).
I’m curious what is your opinion, am I doing something stupid, missing something? Could this be implemented more efficiently?
<Cabbage>
form caption("test sustain/damper pedal") size(400, 300), guiMode("queue"), pluginId("def1")
keyboard bounds(8, 158, 381, 95)
rslider bounds(40, 34, 60, 60) channel("Attack") range(0.001, 5, .01, .2, 1e-6) colour(255, 120, 0, 255) trackerColour(255, 120, 0, 255) markerColour(0, 0, 0, 255) popupText("0")
label bounds(40, 94, 60, 12) text("ATT") fontColour(255, 120, 0, 255) channel("AttackLabel")
rslider bounds(140, 34, 60, 60) channel("Sustain") range(0, .99, .86, 5, 1e-6) colour(150, 255, 0, 255) trackerColour(150, 255, 0, 255) markerColour(0, 0, 0, 255) popupText("0")
label bounds(140, 94, 60, 12) text("SUS") fontColour(150, 255, 0, 255) channel("SustainLabel")
</Cabbage>
<CsoundSynthesizer>
<CsOptions>
-n -d -m0d -+rtmidi=NULL -M0
</CsOptions>
<CsInstruments>
ksmps = 32
nchnls = 2
0dbfs = 1
gkNoteVolume[] init 128 ; track volumes of all notes
instr 1
gaL init 0
gaR init 0
gkSustain = .98 + .02*cabbageGetValue:k("Sustain")
kAttack cabbageGetValue "Attack"
massign 0,0 ; disable automatic triggering of instruments from MIDI note events
gkstatus,kchan,gkdata1,gkdata2 midiin ; monitor MIDI input
insno_Envelope nstrnum "PlayEnvelope" ; assign instrument name to instrument number
insno_Sound nstrnum "PlaySound"
;maxalloc "PlayEnvelope", 64
;maxalloc "PlaySound", 64
;ka active insno_Envelope
;ka active insno_Sound
;printk2 ka
if gkstatus == 144 then // Note ON (or Note OFF when polyphonic aftertouch)
if gkdata2 > 0 then // velocity or aftertouch is not zero
// trigger instrument with a held note, p4 = Note number, p5 = velocity, p6 = attack, p7 = start volume
// the automatic release mechanism cannot be used if a dynamic release via a global sustain/damper pedal is to be applied
// trigger volume envelope - this is needed to prevent clicks when re-triggering the same note before it was turned off
event "i",insno_Envelope+(gkdata1*0.001), 0, -1, gkdata1, gkdata2/127, kAttack, gkNoteVolume[gkdata1]
// trigger oscillator only if its volume is zero (else the volume envelope will be applied to the already running oscillator)
if gkNoteVolume[gkdata1] == 0 then
event "i",insno_Sound+(gkdata1*0.001), 0, -1, gkdata1
endif
endif
endif
endin
instr PlayEnvelope
iVel = p5
iAttack = p6
iStartVol = p7
kON init 1
kAttackEnv init iStartVol
kReleaseEnv init 1
// catch NOTE OFF
if (gkstatus == 128) || (gkstatus == 144 && gkdata2 == 0) then
kON = 0
endif
if kON == 1 then
// linseg doesn't seem to work here
if kAttackEnv <= iVel then
kAttackEnv += 1/(kr*iAttack)
endif
else
if kReleaseEnv > 1e-3 then ; to allow for dynamic damping, can't use expseg (or opcodes with release segment)
kReleaseEnv *= gkSustain
else
kReleaseEnv *= 0.999 ; gentle final release
endif
// set volume to 0 and turn OFF intrument when volume is low enough
if gkNoteVolume[p4] < 1e-5 then
gkNoteVolume[p4] = 0
turnoff
endif
endif
// update global array of volumes
gkNoteVolume[p4] = kAttackEnv*kReleaseEnv
endin
instr PlaySound
iFrq = cpsmidinn(p4)
aVol = tone(a(gkNoteVolume[p4]),33)
aOut poscil aVol, iFrq;, -1, rnd(1)
gaL += aOut
gaR += aOut
if gkNoteVolume[p4] == 0 then
turnoff
endif
endin
instr 999 ;OUTPUT (ALWAYS ON)
outs gaL, gaR ;SEND AUDIO FROM THE FINAL ACTIVE EFFECT TO THE OUTPUTS
clear gaL, gaR
endin
</CsInstruments>
<CsScore>
;causes Csound to run for about 7000 years...
f0 z
i 1 0 -1 ; IN
i 999 0 -1 ; OUT
</CsScore>
</CsoundSynthesizer>