Allow instrument and PolySynth to be scheduled to the transport stop/loop events

addresses #924
This commit is contained in:
Yotam Mann 2021-10-13 19:27:34 -04:00
parent 6dd22e752f
commit 954a4fce37
4 changed files with 168 additions and 19 deletions

View file

@ -83,6 +83,10 @@ export abstract class Instrument<Options extends InstrumentOptions> 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<Options extends InstrumentOptions> 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<Options extends InstrumentOptions> 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

View file

@ -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", () => {

View file

@ -341,10 +341,20 @@ export class PolySynth<Voice extends Monophonic<any> = 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

View file

@ -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(() => {