Distortion Issue w Overlapping Sounds

Overview

I have music playing - sounds fine. I play a little sound effect, also sounds fine.

After playing the little sound several times, the music is ruined with distortion. I have no idea what could be causing this. Note that when I say 'sound effect' I'm referring to a short noise associated with an on-screen event. Not an effect like a filter or delay.

  • Importantly, I can only get this error to occur on-device. I can't recreate it in the simulator.
    Video Evidence of the Bug in Question

Context

Not all of this may be relevant to my issue - but I think more context is better than less right? I've spoilered long bits of code.

I load songs using data from a JSON file paired with a midi file. That JSON file looks like this -

my_song.json

[
{
"bwave": "sine",
"badsr": [20, 63, 35, 35],
"btrackindex": 2,
"bvolume": 0.75,
"awave": "square",
"atrackindex": 3,
"aadsr": [8, 33, 64, 52],
"aenvcurv": 0.15,
"avolume": 0.13,
"swave": "povosim",
"sadsr": [20, 60, 35, 35],
"senvcurv": 0.35,
"svolume": 0.75,
"slegato": true,
"strackindex": 1,
"delaymix": 0.24,
"delayfeedback": 0.33,
"delayaddtap": 0.14,
"bandpassmix": 1,
"bandpassfreq": 680,
"bandpassres": 0.13
}
]

Song objects look like this

Song class definition
class("Song").extends()

function Song:init(name)
  Song.super.init(self)
  self.name = name
  self.midi_path = "assets/midi/"..name..".mid"
  self.channels = {}
end

function Song:play(playlist)
  self.sequence = construct_sequence(self)
  self.sequence:play(end_of_song_handler)
end

Playlists look like this:

Playlist class definition
class("Playlist").extends()

function Playlist:init(name)
  Playlist.super.init(self)
  self.name = name
  self.songs = {}
  self.current_song = nil
  self.pause_music = false
end

My function which constructs songs looks like this

Construct sequence function
local wave_library = {
  sine = sfx.kWaveSine,
  square = sfx.kWaveSquare,
  povosim = sfx.kWavePOVosim
}

function construct_sequence(song)
  -- print("constructing sequence "..song.name)
  local midi = song.midi_path
  local this_sequence = sfx.sequence.new(midi)
  local tempo = this_sequence:getTempo() 
  this_sequence:setTempo(tempo) 
  local this_channel = sfx.channel.new()
  this_channel:setVolume(.90)
  table.insert(song.channels, this_channel)
  local seq_data_path = "data/music/"..song.name..".json"
  local data = json.decodeFile(seq_data_path)
  data = data[1]

  if data.bwave then
    -- construct bass
    local bass_track = this_sequence:getTrackAtIndex(data.btrackindex)
    local bass_synth = sfx.synth.new(wave_library[data.bwave])
    local bass_inst = sfx.instrument.new()
    local a = data.badsr[1]/100
    local d = data.badsr[2]/100
    local s = data.badsr[3]/100
    local r = data.badsr[4]/100
    bass_synth:setADSR(a,d,s,r)
    local volume = .9 or data.bvolume
    bass_synth:setVolume(volume)
    bass_inst:addVoice(bass_synth)
    bass_track:setInstrument(bass_inst)
    this_channel:addSource(bass_inst)
  end

  if data.twave then
    -- construct tenor
  end

  if data.awave then
    -- construct alto
    local alto_track = this_sequence:getTrackAtIndex(data.atrackindex)
    local alto_synth = sfx.synth.new(wave_library[data.awave])
    local alto_instrument = sfx.instrument.new()
    local a = data.aadsr[1]/100
    local d = data.aadsr[2]/100
    local s = data.aadsr[3]/100
    local r = data.aadsr[4]/100
    alto_synth:setADSR(a,d,s,r)
    local volume = .9 or data.avolume
    alto_synth:setVolume(volume)
    alto_synth:setEnvelopeCurvature(data.aenvcurv)
    alto_instrument:addVoice(alto_synth)
    alto_track:setInstrument(alto_instrument)
    this_channel:addSource(alto_instrument)
  end

  if data.swave then
    -- construct soprano
    local soprano_track = this_sequence:getTrackAtIndex(data.strackindex)
    local soprano_synth = sfx.synth.new(wave_library[data.swave])
    local soprano_instrument = sfx.instrument.new()
    local a = data.sadsr[1]/100 
    local d = data.sadsr[2]/100 
    local s = data.sadsr[3]/100 
    local r = data.sadsr[4]/100 
    soprano_synth:setADSR(a,d,s,r)
    soprano_synth:setEnvelopeCurvature(data.senvcurv)
    local volume = .9 or data.svolume
    soprano_synth:setVolume(volume)
    soprano_instrument:addVoice(soprano_synth)
    soprano_track:setInstrument(soprano_instrument)
    this_channel:addSource(soprano_instrument)
  end

  if data.ppolyphony then
    local track_index = data.ptrackindex
    local piano_track = this_sequence:getTrackAtIndex(track_index)
    local volume = data.pvolume
    local polyphony = data.ppolyphony
    local synth_stack = table.create(polyphony, 0)
    local piano_inst = sfx.instrument.new()
    for i=1, polyphony do
      local synth = sfx.synth.new(sfx.kWaveSine)
      local attack_variance = math.random(50, 100)
      attack_variance *= .01
      local decay_variance = (attack_variance * 10)/2
      local sustain_variance = attack_variance * 5
      local release_variance = sustain_variance
      synth:setADSR(attack_variance, decay_variance, sustain_variance, release_variance)
      synth:setEnvelopeCurvature(.15)
      local variance = math.random(15, 24)
      variance *= .01
      -- print("variance: "..variance)
      synth:setVolume(variance) 
      piano_inst:addVoice(synth)
    end
    piano_track:setInstrument(piano_inst)
    this_channel:addSource(piano_inst)
  end

  if data.delaymix then
    local delay = sfx.delayline.new(.55)
    delay:setMix(data.delaymix)
    delay:setFeedback(data.delayfeedback)
    delay:addTap(data.delayaddtap)
    this_channel:addEffect(delay)
  end

  if data.bandpassmix then
    local bandpass = sfx.twopolefilter.new("bandpass")
    bandpass:setMix(data.bandpassmix)
    bandpass:setFrequency(data.bandpassfreq)
    bandpass:setResonance(data.bandpassres)
    this_channel:addEffect(bandpass)
  end
  
  return this_sequence
end

And my sound effect

Sound Effect
-- timer to try to prevent overlapping the noise on itself
local nav_sound_playing = false
function new_nav_sound()
  if nav_sound_playing then
  else
    nav_sound()
    nav_sound_playing = true
    pd.timer.new(250, function()
      nav_sound_playing = false
    end)
  end
end

function nav_sound()
  my_sfx_channel = sfx.channel.new()
  local bandpass = sfx.twopolefilter.new('bandpass')
  bandpass:setMix(1)
  bandpass:setFrequency(800)
  bandpass:setResonance(.06)
  my_sfx_channel:addEffect(bandpass)
  local noise_synth = sfx.synth.new(sfx.kWaveNoise)
  local noise_inst = sfx.instrument.new()
  noise_synth:setADSR(0, .01, .01, .01)
  noise_synth:setEnvelopeCurvature(.05)
  noise_synth:setVolume(.84)
  noise_synth:setLegato(true)
  noise_inst:addVoice(noise_synth)
  local frequency = 'Eb4'
  local velocity = .42
  local length = .07
  local digi_synth = sfx.synth.new(sfx.kWavePODigital)
  digi_synth:setParameter(1, .8)
  digi_synth:setParameter(2, .8)
  local digi_inst = sfx.instrument.new()
  digi_synth:setADSR(.01, .02, .10, .01)
  digi_synth:setEnvelopeCurvature(.05)
  digi_synth:setVolume(.88)
  digi_synth:setLegato(true)
  digi_inst:addVoice(digi_synth)
  frequency = 'Bb3'
  velocity = .72
  length = .09
  my_sfx_channel:addSource(noise_inst)
  my_sfx_channel:addSource(digi_inst)
  my_sfx_channel:setVolume(0.55)
  digi_inst:playNote(frequency, velocity,length) 
  noise_inst:playNote(frequency, velocity,length) 
end

See For Yourself

Navigate to the directory which contains the 'source' folder called 'scapia.'
Type make long > enter
If you just crank the crank or spam up or down and get that little navigation click sound to play a bunch, the music will start to distort into just a nasty mess. I'm so curious what could be going on here.
scapia-main-3.zip (501.8 KB)

Final Thoughts

  • Am I messing with my music channel somehow when playing this sound effect?
  • Am I making a mistake by instantiating new instruments/channels/synths/effects every time I play the sound effect? Are those supposed to be pre-made by the time it's time to make a noise?
  • Is my sequence somehow getting out of step with itself?

I think you're right that it's related to creating new instruments/channels/synths every time the effect plays - particularly because it seems like you're never deleting them! That makes it seem like there are more and more and more channels/instruments/synths over time, and at some point the playdate's audio routine can't work fast enough (I assume a big part of this is the filter: even when your synth is done making noise, the channel is still active, and is likely still processing that band-pass).

Your idea about pre-making the channel/instrument/synth seems good to me: you could do that setup when the game starts up, and then when you actually want to hear a sound, you'd just call the playNote() function on an instrument which is already present.

1 Like

Next time I sit down to code i’ll try using a single instance of an sfx channel and the instruments therein. I’ll report back if that’s fixes it.

Thanks for your thoughts :thought_balloon:

1 Like

hey thanks for the input, it worked.

1 Like

Awesome, happy to hear it!

1 Like