Load sample based instruments in a memory-efficient way

When we look at the MIDIPlayer sample, we see the following:

	local inst = snd.instrument.new()
	inst:addVoice(drumsynth("drums/kick"), 35)
	inst:addVoice(drumsynth("drums/kick"), 36)
	inst:addVoice(drumsynth("drums/snare"), 38)
	inst:addVoice(drumsynth("drums/clap"), 39)
	inst:addVoice(drumsynth("drums/tom-low"), 41)
	inst:addVoice(drumsynth("drums/tom-low"), 43)
	inst:addVoice(drumsynth("drums/tom-mid"), 45)
	inst:addVoice(drumsynth("drums/tom-mid"), 47)

If I understand correctly, some of these samples are duplicated in RAM. The docs state that there is a rangeend parameter. Can both kick lines be replaced by

inst:addVoice(drumsynth("drums/kick"), 35, 36)

without side effects? Note that all voices in my instrument use the same settings like attack, sustain etc.

Second question: If I have multiple drum instruments in a Sequence, both instruments and having their own attack and sustain values; is it then still possible to share the RAM for the sample? Again, in my game i set attack, sustain etc equal for all voices in an instrument, but the two drum tracks can have different settings

You're right, that's loading those samples twice. I'd guess my thinking (if I was thinking at all) was that demonstrates that they're actually separate instruments in the midi file and you can easily swap, say, note 36 for a different sound without changing 35. They're all pretty small samples so I wasn't worried much about memory here.

And yes, you can have multiple sample-based synths using the same sample data while changing their parameters separately.

Here we've got a single instance of the kick drum sample but the second synth has a different envelope to change the dynamics:

snd = playdate.sound
sample = snd.sample.new("KickDrum")

s1 = snd.synth.new(sample)

s2 = snd.synth.new(sample)
s2:setAttack(0.1)
s2:setDecay(0.1)
s2:setSustain(0)

function playdate.BButtonDown() s1:playNote("C4") end
function playdate.AButtonDown() s2:playNote("C4") end

As an aside, I see/hear two weird things happening in this demo: First, even though the synth's "legato" parameter is off by default, it's still acting like it's on--if you hit the trigger on s2 before the previous note is done playing, it plays at a lower level. It looks like this is because the envelope's "retrigger" setting is also off by default. Kind of a contradiction there. I'm not sure if there's a difference between legato=on and retrigger=off? I don't know what I was thinking when I implemented those.. My only guess is I did one and then forgot about it by the time I did the other.

The other is that even with attack=0, s2's attack is muted. There's some ramping code in there to avoid popping but it's not working right in this case because the note isn't ending correctly, it's continuing to "play" even when the sample's finished. As a result, when the next note starts up it has to ramp the output from 0 to 1.

I've got those filed, hope to have a chance to address them at some point. :sweat_smile:

2 Likes

+1 -- Working on a little music toy game and I just ran into the legato issue. The legato setting seems to affect note playback a tiny bit... however it is nothing like how I would expect a legato instrument to behave. I tried the setting on synths with various waveforms but the results were the same.

Example code of broken legato:

local sequence = nil
local track = nil
local synth = nil

function init()
  sequence = playdate.sound.sequence.new()
  synth = playdate.sound.synth.new()
  
  track = playdate.sound.track.new()
  track:setInstrument(synth)
  track:addNote(1, "C3", 20)
  track:addNote(3, "E3", 2)
  track:addNote(5, "G3", 2)
  track:addNote(7, "F3", 1)
  track:addNote(8, "D3", 1)
  track:addNote(9, "B3", 10)
  track:addNote(15, "C4", 1)
  
  sequence:setTrackAtIndex(1, track)
  
  sequence:play()
  sequence:setTempo(5)
  sequence:setLoops(0)
end

init()

playdate.inputHandlers.push({
    AButtonUp = function() synth:setLegato(true) end,
    BButtonUp = function() synth:setLegato(false) end,
})
1 Like