Search code examples
javascripttypescriptaudiotone.js

Tone.js completely stop all playing sounds


In short on a button press I'd like to play a few notes using a PolySynth and a Sequence. If the user repeatedly presses the button I'd like whatever is playing to be stopped, and started again.

The issue: No matter what I try I cannot completely cancel/silence the previously played notes in case the sequence is started again (button clicked again). This is most likely because of either the envelope's decay/sustain.

My Synth:

import { PolySynth } from 'tone'

const synth = new PolySynth(Synth, {
  oscillator: {
    type: 'sine4',
    volume: -6,
  },
  envelope: {
    attack: 0.01,
    decay: 0.5,
    sustain: 0.1,
    release: 1,
  },
}).toDestination()
synth.maxPolyphony = 4 // max notes playing at a time, not sure if necessary

My Sequence:

import { Sequence } from 'tone'

// Play the 2 notes individually then play them together
const notes = [
  { note: 'C4', duration: '8n' },
  { note: 'G4', duration: '8n' },
  { note: ['C4', 'G4'], duration: '4n' }
]

// The sequence that should play the notes after one another
const sequence = new Sequence({
  subdivision: '8n',
  loop: false,
  events: notes,
  callback: (time, note) => synth.triggerAttackRelease(note.note, note.duration, time),
})

The way I play it, this is an event handler:

import { start, Transport } from 'tone'

// Event handler simply attached to a button's onClick
function onButtonClicked() {
  // Call whatever this start is, doc says it can only happen in an event handler
  start()
  
  // Try everything to kill current sound
  Transport.cancel()
  Transport.stop()

  // Start it again
  Transport.start()
  sequence.start()
}

How could I completely kill all sound (if there is any) before starting to play it?


Solution

  • Short Answer

    Thinking about this quite a bit, If I understand you correctly this actually is intended behavior. You are triggering a a note on a Synth (which is basically an AudioWorkletNode). So as soon as a note triggered the synth, the note is gone. The only way to stop that note from playing would be to mute the synth itself.

    Long Answer

    In the comments you said, that you might missing something conceptually and I think you are on the right track with that.

    Let's think about how a sound is generated with MIDI.

    1. You are connecting a Synth (which takes MIDI notes and generates Sound) to an output
    2. You are scheduling some MIDI notes on the transport
    3. You start the transport
    4. As soon as the transport hits the scheduled time for a note, that MIDI value will be sent to the Synth.
    5. Since the Synth is basically an AudioWorkletNode with an Envelope Generator, the Synth takes that MIDI note and triggers the internal sound generation (via the envelope). So a MIDI note at a specific point in time triggers a specific length of sound generation (which would be the ADS part). Even if the MIDI notes duration is only 1ms long in your example, the sound generation would hold on for at least 1.001 seconds (Release plus 1 milliseconds MIDI duration). Let's break this down a bit more:
      • The MIDI note has a start and end point on the imaginary transport timeline.
      • Start triggers the ADS part of the Envelope.
      • End triggers the R part of the Envelope.
      • Once your MIDI note triggered the Envelope the sound gets generated.

    So when you stop your transport or the sequence itself, what would that do? If a MIDI note already triggered the Envelope, the Envelope will receive the MIDI end trigger and trigger the Release envelope.

    So there will always be a tailing sound of your Synth, because the MIDI note does not determine your start and end point of your Synth, but triggers parts of your Envelope. So actually your Synth creates the sound and that is neither tied to the transport, nor can it be.

    Hope that explanation helped you a bit. If I misunderstood you, I am happy to correct.