2019-08-12 04:16:03 +00:00
|
|
|
import { expect } from "chai";
|
|
|
|
import { BasicTests } from "test/helper/Basic";
|
|
|
|
import { CompareToFile } from "test/helper/CompareToFile";
|
|
|
|
import { InstrumentTest } from "test/helper/InstrumentTests";
|
|
|
|
import { atTime, Offline } from "test/helper/Offline";
|
|
|
|
import { OutputAudio } from "test/helper/OutputAudio";
|
|
|
|
import { PolySynth } from "./PolySynth";
|
|
|
|
import { Synth } from "./Synth";
|
|
|
|
|
|
|
|
describe("PolySynth", () => {
|
|
|
|
|
|
|
|
BasicTests(PolySynth);
|
|
|
|
|
|
|
|
it("matches a file", () => {
|
|
|
|
return CompareToFile(() => {
|
2019-09-03 23:29:59 +00:00
|
|
|
const synth = new PolySynth().toDestination();
|
2019-08-12 04:16:03 +00:00
|
|
|
synth.triggerAttackRelease("C4", 0.2, 0);
|
|
|
|
synth.triggerAttackRelease("C4", 0.1, 0.1);
|
|
|
|
synth.triggerAttackRelease("E4", 0.1, 0.2);
|
|
|
|
synth.triggerAttackRelease("E4", 0.1, 0.3);
|
|
|
|
synth.triggerAttackRelease("G4", 0.1, 0.4);
|
|
|
|
synth.triggerAttackRelease("B4", 0.1, 0.4);
|
|
|
|
synth.triggerAttackRelease("C4", 0.2, 0.5);
|
|
|
|
}, "polySynth.wav", 0.6);
|
|
|
|
});
|
|
|
|
|
|
|
|
it("matches another file", () => {
|
|
|
|
return CompareToFile(() => {
|
2019-09-03 23:29:59 +00:00
|
|
|
const synth = new PolySynth().toDestination();
|
2019-08-12 04:16:03 +00:00
|
|
|
synth.triggerAttackRelease(["C4", "E4", "G4", "B4"], 0.2, 0);
|
|
|
|
synth.triggerAttackRelease(["C4", "E4", "G4", "B4"], 0.2, 0.3);
|
|
|
|
}, "polySynth2.wav", 0.6);
|
|
|
|
});
|
|
|
|
|
|
|
|
it("matches a file and chooses the right voice", () => {
|
|
|
|
return CompareToFile(() => {
|
2019-09-03 23:29:59 +00:00
|
|
|
const synth = new PolySynth().toDestination();
|
2019-08-12 04:16:03 +00:00
|
|
|
synth.triggerAttackRelease(["C4", "E4"], 1, 0);
|
|
|
|
synth.triggerAttackRelease("G4", 0.1, 0.2);
|
|
|
|
synth.triggerAttackRelease("B4", 0.1, 0.4);
|
|
|
|
synth.triggerAttackRelease("G4", 0.1, 0.6);
|
|
|
|
}, "polySynth3.wav", 0.5);
|
|
|
|
});
|
|
|
|
|
|
|
|
context("Playing Notes", () => {
|
|
|
|
|
|
|
|
it("triggerAttackRelease can take an array of durations", () => {
|
|
|
|
return OutputAudio(() => {
|
2019-09-03 23:29:59 +00:00
|
|
|
const polySynth = new PolySynth();
|
2019-08-12 04:16:03 +00:00
|
|
|
polySynth.toDestination();
|
|
|
|
polySynth.triggerAttackRelease(["C4", "D4"], [0.1, 0.2]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it("triggerAttack and triggerRelease can be invoked without arrays", () => {
|
|
|
|
return Offline(() => {
|
2019-09-03 23:29:59 +00:00
|
|
|
const polySynth = new PolySynth();
|
2019-08-12 04:16:03 +00:00
|
|
|
polySynth.set({ envelope: { release : 0.1 } });
|
|
|
|
polySynth.toDestination();
|
|
|
|
polySynth.triggerAttack("C4", 0);
|
|
|
|
polySynth.triggerRelease("C4", 0.1);
|
|
|
|
}, 0.3).then(buffer => {
|
|
|
|
expect(buffer.getTimeOfFirstSound()).to.be.closeTo(0, 0.01);
|
|
|
|
expect(buffer.getValueAtTime(0.2)).to.be.closeTo(0, 0.01);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it("can stop all of the currently playing sounds", () => {
|
|
|
|
return Offline(() => {
|
2019-09-03 23:29:59 +00:00
|
|
|
const polySynth = new PolySynth();
|
2019-08-12 04:16:03 +00:00
|
|
|
polySynth.set({ envelope: { release : 0.1 } });
|
|
|
|
polySynth.toDestination();
|
|
|
|
polySynth.triggerAttack(["C4", "E4", "G4", "B4"], 0);
|
2019-08-13 23:54:11 +00:00
|
|
|
return atTime(0.1, () => {
|
|
|
|
polySynth.releaseAll();
|
|
|
|
});
|
2019-08-12 04:16:03 +00:00
|
|
|
}, 0.3).then((buffer) => {
|
|
|
|
expect(buffer.getTimeOfFirstSound()).to.be.closeTo(0, 0.01);
|
|
|
|
expect(buffer.getTimeOfLastSound()).to.be.closeTo(0.2, 0.01);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it("is silent before being triggered", () => {
|
|
|
|
return Offline(() => {
|
2019-09-03 23:29:59 +00:00
|
|
|
const polySynth = new PolySynth();
|
2019-08-12 04:16:03 +00:00
|
|
|
polySynth.toDestination();
|
|
|
|
}).then((buffer) => {
|
|
|
|
expect(buffer.isSilent()).to.be.true;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it("can be scheduled to start in the future", () => {
|
|
|
|
return Offline(() => {
|
2019-09-03 23:29:59 +00:00
|
|
|
const polySynth = new PolySynth();
|
2019-08-12 04:16:03 +00:00
|
|
|
polySynth.toDestination();
|
|
|
|
polySynth.triggerAttack("C4", 0.1);
|
|
|
|
}, 0.3).then((buffer) => {
|
|
|
|
expect(buffer.getTimeOfFirstSound()).to.be.closeTo(0.1, 0.01);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-09-04 03:07:44 +00:00
|
|
|
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, {
|
|
|
|
envelope : {
|
|
|
|
release: 0.1,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
polySynth.toDestination();
|
|
|
|
polySynth.triggerAttackRelease(["C4", "E4", "G4", "B4", "D5"], 0.1, 0);
|
|
|
|
return [
|
|
|
|
atTime(0, () => {
|
|
|
|
expect(polySynth.activeVoices).to.equal(5);
|
|
|
|
}),
|
|
|
|
atTime(0.3, () => {
|
|
|
|
expect(polySynth.activeVoices).to.equal(0);
|
|
|
|
}),
|
|
|
|
];
|
|
|
|
}, 10);
|
|
|
|
});
|
|
|
|
|
2019-08-12 17:21:55 +00:00
|
|
|
it("warns when too much polyphony is attempted and notes are dropped", () => {
|
|
|
|
let wasInvoked = false;
|
|
|
|
const originalWarn = console.warn;
|
|
|
|
console.warn = () => wasInvoked = true;
|
|
|
|
return Offline(() => {
|
2019-09-03 23:29:59 +00:00
|
|
|
const polySynth = new PolySynth({
|
|
|
|
maxPolyphony: 2,
|
|
|
|
});
|
2019-08-12 17:21:55 +00:00
|
|
|
polySynth.toDestination();
|
|
|
|
polySynth.triggerAttack(["C4", "D4", "G4"], 0.1);
|
|
|
|
}, 0.3).then((buffer) => {
|
|
|
|
expect(wasInvoked).to.equal(true);
|
|
|
|
console.warn = originalWarn;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-08-12 04:16:03 +00:00
|
|
|
it("reports the active notes", () => {
|
|
|
|
return Offline(() => {
|
2019-09-03 23:29:59 +00:00
|
|
|
const polySynth = new PolySynth();
|
2019-08-12 04:16:03 +00:00
|
|
|
polySynth.set({ envelope: { release : 0.1 } });
|
|
|
|
polySynth.toDestination();
|
|
|
|
polySynth.triggerAttackRelease("C4", 0.1, 0.1);
|
|
|
|
polySynth.triggerAttackRelease("D4", 0.1, 0.2);
|
2019-08-12 17:18:18 +00:00
|
|
|
polySynth.triggerAttackRelease("C4", 0.1, 0.5);
|
|
|
|
polySynth.triggerAttackRelease("C4", 0.1, 0.6);
|
2019-08-12 04:16:03 +00:00
|
|
|
return [
|
2019-08-12 17:18:18 +00:00
|
|
|
atTime(0, () => {
|
|
|
|
expect(polySynth.activeVoices).to.equal(0);
|
|
|
|
}),
|
2019-08-12 04:16:03 +00:00
|
|
|
atTime(0.1, () => {
|
2019-08-12 17:18:18 +00:00
|
|
|
expect(polySynth.activeVoices).to.equal(1);
|
|
|
|
}),
|
|
|
|
atTime(0.2, () => {
|
2019-08-12 04:16:03 +00:00
|
|
|
expect(polySynth.activeVoices).to.equal(2);
|
|
|
|
}),
|
|
|
|
atTime(0.3, () => {
|
|
|
|
expect(polySynth.activeVoices).to.equal(1);
|
|
|
|
}),
|
|
|
|
atTime(0.4, () => {
|
|
|
|
expect(polySynth.activeVoices).to.equal(0);
|
|
|
|
}),
|
2019-08-12 17:18:18 +00:00
|
|
|
atTime(0.5, () => {
|
|
|
|
expect(polySynth.activeVoices).to.equal(1);
|
|
|
|
}),
|
|
|
|
atTime(0.6, () => {
|
|
|
|
expect(polySynth.activeVoices).to.equal(1);
|
|
|
|
}),
|
|
|
|
atTime(0.7, () => {
|
|
|
|
expect(polySynth.activeVoices).to.equal(1);
|
|
|
|
}),
|
|
|
|
atTime(0.8, () => {
|
|
|
|
expect(polySynth.activeVoices).to.equal(0);
|
|
|
|
}),
|
2019-08-12 04:16:03 +00:00
|
|
|
];
|
|
|
|
}, 1);
|
|
|
|
});
|
|
|
|
|
2019-08-14 14:50:01 +00:00
|
|
|
it("can trigger another attack before the release has ended", () => {
|
|
|
|
// compute the end time
|
|
|
|
return Offline(() => {
|
2019-09-03 23:29:59 +00:00
|
|
|
const synth = new PolySynth(Synth, {
|
2019-08-14 14:50:01 +00:00
|
|
|
envelope : {
|
|
|
|
release: 0.1,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
synth.toDestination();
|
|
|
|
synth.triggerAttack("C4", 0.05);
|
|
|
|
synth.triggerRelease("C4", 0.1);
|
|
|
|
synth.triggerAttack("C4", 0.15);
|
|
|
|
synth.triggerRelease("C4", 0.2);
|
|
|
|
}, 1).then((buffer) => {
|
|
|
|
expect(buffer.getTimeOfLastSound()).to.be.closeTo(0.3, 0.01);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it("can trigger another attack right after the release has ended", () => {
|
|
|
|
// compute the end time
|
|
|
|
return Offline(() => {
|
2019-09-03 23:29:59 +00:00
|
|
|
const synth = new PolySynth(Synth, {
|
2019-08-14 14:50:01 +00:00
|
|
|
envelope : {
|
|
|
|
release: 0.1,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
synth.toDestination();
|
|
|
|
synth.triggerAttack("C4", 0.05);
|
|
|
|
synth.triggerRelease("C4", 0.1);
|
|
|
|
synth.triggerAttack("C4", 0.2);
|
|
|
|
synth.triggerRelease("C4", 0.3);
|
|
|
|
return atTime(0.41, () => {
|
|
|
|
expect(synth.activeVoices).to.equal(0);
|
|
|
|
});
|
|
|
|
}, 1).then((buffer) => {
|
|
|
|
expect(buffer.getTimeOfLastSound()).to.be.closeTo(0.4, 0.01);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-08-12 04:16:03 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
context("API", () => {
|
|
|
|
|
|
|
|
it("can be constructed with an options object", () => {
|
2019-09-03 23:29:59 +00:00
|
|
|
const polySynth = new PolySynth(Synth, {
|
2019-08-12 04:16:03 +00:00
|
|
|
envelope : {
|
|
|
|
sustain : 0.3,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
expect(polySynth.get().envelope.sustain).to.equal(0.3);
|
|
|
|
polySynth.dispose();
|
|
|
|
});
|
|
|
|
|
2019-08-14 14:50:01 +00:00
|
|
|
it("can pass in the volume", () => {
|
2019-08-12 04:16:03 +00:00
|
|
|
const polySynth = new PolySynth({
|
|
|
|
volume : -12,
|
|
|
|
});
|
|
|
|
expect(polySynth.volume.value).to.be.closeTo(-12, 0.1);
|
|
|
|
polySynth.dispose();
|
|
|
|
});
|
|
|
|
|
|
|
|
it("can get/set attributes", () => {
|
|
|
|
const polySynth = new PolySynth();
|
|
|
|
polySynth.set({
|
|
|
|
envelope : {decay : 3},
|
|
|
|
});
|
|
|
|
expect(polySynth.get().envelope.decay).to.equal(3);
|
|
|
|
polySynth.dispose();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|