Tone.js completely stop all playing sounds

3.7k Views Asked by At

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?

5

There are 5 best solutions below

0
On BEST ANSWER

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.

0
On

Use Tone.Transport.pause()

See example below if you have a play/pause button with class of 'pause-play':

$( '.pause-play' ).on('click', function(e) {

    if (Tone.Transport.state === "paused" ) {  
          Tone.Transport.start("+0.1") })
    else {
          Tone.Transport.pause()  }
    }
)
0
On

The note keeps playing because the transport stops after the note was triggered and before it was released. Therefore a solution is to trigger the release of all notes when you push the stop button. I hit a similar problem while using tambien/piano (based on Tonejs).

    Tone.Transport.toggle()
    if (Tone.Transport.state === 'stopped') {
      for (let i=9; i<97; i++) {
        piano.keyUp({midi: i}, '+0')
      }
    }
0
On

That solution could be difficult to implement in longer sequences, but in your case it should work. I've challenged quite similar problem and it worked.

The problem with the polySynth is that you can only add notes, which are played. But with the normal synth it is possible to "kill the sound" by overwriting the played note by the empty one.

// It would create continuously playing note.
synth = synth || new Tone.Synth().toMaster();
synth.triggerAttackRelease(noteToPlay);

// It would mute the sound.
synth = synth || new Tone.Synth().toMaster();
synth.triggerAttackRelease();

It is possible to play many single synths simultaneously, so you can create the polyphonic synth manually.

synth1 = synth1 || new Tone.Synth().toMaster();
synth1.triggerAttackRelease(noteToPlay);
synth2 = synth2 || new Tone.Synth().toMaster();
synth2.triggerAttackRelease(noteToPlay2);
synth3 = synth3 || new Tone.Synth().toMaster();
synth3.triggerAttackRelease(noteToPlay3);

Building the sequence would be more complicated, but you will be able to mute the playing sequence by adding the "soundKiller" at the beginning of the play function. What's important: instead of declaring the sequence and then play it by calling "Transport", just play the notes inside the event handler (because - as another person has already written - transport can't be stopped after calling it).

0
On

Just call the releaseAll() method on the PolySynth object:

polySynth.releaseAll()

This stops all currently triggered notes from playing.