diff --git a/Tone/instrument/Instrument.ts b/Tone/instrument/Instrument.ts index f7916ce6..def4a98f 100644 --- a/Tone/instrument/Instrument.ts +++ b/Tone/instrument/Instrument.ts @@ -83,6 +83,10 @@ export abstract class Instrument extends Tone if (this._syncState()) { this._syncMethod("triggerAttack", 1); this._syncMethod("triggerRelease", 0); + + this.context.transport.on("stop", this._syncedRelease); + this.context.transport.on("pause", this._syncedRelease); + this.context.transport.on("loopEnd", this._syncedRelease); } return this; } @@ -126,6 +130,10 @@ export abstract class Instrument extends Tone this._synced = false; this.triggerAttack = this._original_triggerAttack; this.triggerRelease = this._original_triggerRelease; + + this.context.transport.off("stop", this._syncedRelease); + this.context.transport.off("pause", this._syncedRelease); + this.context.transport.off("loopEnd", this._syncedRelease); } return this; } @@ -166,6 +174,11 @@ export abstract class Instrument extends Tone abstract triggerRelease(...args: any[]): this; private _original_triggerRelease = this.triggerRelease; + /** + * The release which is scheduled to the timeline. + */ + protected _syncedRelease = (time: number) => this._original_triggerRelease(time); + /** * clean up * @returns {Instrument} this diff --git a/Tone/instrument/PolySynth.test.ts b/Tone/instrument/PolySynth.test.ts index a9013720..d3d022ec 100644 --- a/Tone/instrument/PolySynth.test.ts +++ b/Tone/instrument/PolySynth.test.ts @@ -135,23 +135,6 @@ describe("PolySynth", () => { }); }); - it("can be synced to the transport", () => { - return Offline(({ transport }) => { - const polySynth = new PolySynth(Synth, { - envelope: { - release: 0.1, - }, - }).sync(); - polySynth.toDestination(); - polySynth.triggerAttackRelease("C4", 0.1, 0.1); - polySynth.triggerAttackRelease("E4", 0.1, 0.3); - transport.start(0.1); - }, 0.8).then((buffer) => { - expect(buffer.getTimeOfFirstSound()).to.be.closeTo(0.2, 0.01); - expect(buffer.getTimeOfLastSound()).to.be.closeTo(0.6, 0.01); - }); - }); - it("disposes voices when they are no longer used", () => { return Offline(() => { const polySynth = new PolySynth(Synth, { @@ -266,6 +249,86 @@ describe("PolySynth", () => { }); + context("Transport sync", () => { + it("can be synced to the transport", () => { + return Offline(({ transport }) => { + const polySynth = new PolySynth(Synth, { + envelope: { + release: 0.1, + }, + }).sync(); + polySynth.toDestination(); + polySynth.triggerAttackRelease("C4", 0.1, 0.1); + polySynth.triggerAttackRelease("E4", 0.1, 0.3); + transport.start(0.1); + }, 0.8).then((buffer) => { + expect(buffer.getTimeOfFirstSound()).to.be.closeTo(0.2, 0.01); + expect(buffer.getTimeOfLastSound()).to.be.closeTo(0.6, 0.01); + }); + }); + + it("is silent until the transport is started", () => { + return Offline(({ transport }) => { + const synth = new PolySynth(Synth).sync().toDestination(); + synth.triggerAttackRelease("C4", 0.5); + transport.start(0.5); + }, 1).then((buffer) => { + expect(buffer.getTimeOfFirstSound()).is.closeTo(0.5, 0.1); + }); + }); + + it("stops when the transport is stopped", () => { + return Offline(({ transport }) => { + const synth = new PolySynth(Synth, { + envelope: { + release: 0 + } + }).sync().toDestination(); + synth.triggerAttackRelease("C4", 0.5); + transport.start(0.5).stop(1); + }, 1.5).then((buffer) => { + expect(buffer.getTimeOfLastSound()).is.closeTo(1, 0.1); + }); + }); + + it("goes silent at the loop boundary", () => { + return Offline(({ transport }) => { + const synth = new PolySynth(Synth, { + envelope: { + release: 0 + } + }).sync().toDestination(); + synth.triggerAttackRelease("C4", 0.8, 0.5); + transport.loopEnd = 1; + transport.loop = true; + transport.start(); + }, 2).then((buffer) => { + expect(buffer.getRmsAtTime(0)).to.be.closeTo(0, 0.05); + expect(buffer.getRmsAtTime(0.6)).to.be.closeTo(0.2, 0.05); + expect(buffer.getRmsAtTime(1.1)).to.be.closeTo(0, 0.05); + expect(buffer.getRmsAtTime(1.6)).to.be.closeTo(0.2, 0.05); + }); + }); + + it("can unsync", () => { + return Offline(({ transport }) => { + const synth = new PolySynth(Synth, { + envelope: { + sustain: 1, + release: 0 + } + }).sync().toDestination().unsync(); + synth.triggerAttackRelease("C4", 1, 0.5); + transport.start().stop(1); + }, 2).then((buffer) => { + expect(buffer.getRmsAtTime(0)).to.be.closeTo(0, 0.05); + expect(buffer.getRmsAtTime(0.6)).to.be.closeTo(0.6, 0.05); + expect(buffer.getRmsAtTime(1.4)).to.be.closeTo(0.6, 0.05); + expect(buffer.getRmsAtTime(1.6)).to.be.closeTo(0, 0.05); + }); + }); + }); + context("API", () => { it("can be constructed with an options object", () => { @@ -282,7 +345,7 @@ describe("PolySynth", () => { expect(() => { // @ts-ignore new PolySynth(PluckSynth); - }).throws(Error) + }).throws(Error); }); it("can pass in the volume", () => { diff --git a/Tone/instrument/PolySynth.ts b/Tone/instrument/PolySynth.ts index 90229b40..afe169b5 100644 --- a/Tone/instrument/PolySynth.ts +++ b/Tone/instrument/PolySynth.ts @@ -341,10 +341,20 @@ export class PolySynth = Synth> extends Instrument if (this._syncState()) { this._syncMethod("triggerAttack", 1); this._syncMethod("triggerRelease", 1); + + // make sure that the sound doesn't play after its been stopped + this.context.transport.on("stop", this._syncedRelease); + this.context.transport.on("pause", this._syncedRelease); + this.context.transport.on("loopEnd", this._syncedRelease); } return this; } + /** + * The release which is scheduled to the timeline. + */ + protected _syncedRelease = (time: number) => this.releaseAll(time); + /** * Set a member/attribute of the voices * @example @@ -382,7 +392,7 @@ export class PolySynth = Synth> extends Instrument }); return this; } - + dispose(): this { super.dispose(); this._dummyVoice.dispose(); diff --git a/Tone/instrument/Synth.test.ts b/Tone/instrument/Synth.test.ts index d907fa93..43610711 100644 --- a/Tone/instrument/Synth.test.ts +++ b/Tone/instrument/Synth.test.ts @@ -106,6 +106,69 @@ describe("Synth", () => { }); }); + context("Transport sync", () => { + it("is silent until the transport is started", () => { + return Offline(({ transport }) => { + const synth = new Synth().sync().toDestination(); + synth.triggerAttackRelease("C4", 0.5); + transport.start(0.5); + }, 1).then((buffer) => { + expect(buffer.getTimeOfFirstSound()).is.closeTo(0.5, 0.1); + }); + }); + + it("stops when the transport is stopped", () => { + return Offline(({ transport }) => { + const synth = new Synth({ + envelope: { + release: 0 + } + }).sync().toDestination(); + synth.triggerAttackRelease("C4", 0.5); + transport.start(0.5).stop(1); + }, 1.5).then((buffer) => { + expect(buffer.getTimeOfLastSound()).is.closeTo(1, 0.1); + }); + }); + + it("goes silent at the loop boundary", () => { + return Offline(({ transport }) => { + const synth = new Synth({ + envelope: { + release: 0 + } + }).sync().toDestination(); + synth.triggerAttackRelease("C4", 0.8, 0.5); + transport.loopEnd = 1; + transport.loop = true; + transport.start(); + }, 2).then((buffer) => { + expect(buffer.getRmsAtTime(0)).to.be.closeTo(0, 0.05); + expect(buffer.getRmsAtTime(0.6)).to.be.closeTo(0.2, 0.05); + expect(buffer.getRmsAtTime(1.1)).to.be.closeTo(0, 0.05); + expect(buffer.getRmsAtTime(1.6)).to.be.closeTo(0.2, 0.05); + }); + }); + + it("can unsync", () => { + return Offline(({ transport }) => { + const synth = new Synth({ + envelope: { + sustain: 1, + release: 0 + } + }).sync().toDestination().unsync(); + synth.triggerAttackRelease("C4", 1, 0.5); + transport.start().stop(1); + }, 2).then((buffer) => { + expect(buffer.getRmsAtTime(0)).to.be.closeTo(0, 0.05); + expect(buffer.getRmsAtTime(0.6)).to.be.closeTo(0.6, 0.05); + expect(buffer.getRmsAtTime(1.4)).to.be.closeTo(0.6, 0.05); + expect(buffer.getRmsAtTime(1.6)).to.be.closeTo(0, 0.05); + }); + }); + }); + context("Portamento", () => { it("can play notes with a portamento", () => { return Offline(() => {