2019-08-12 04:16:03 +00:00
|
|
|
import { expect } from "chai";
|
2019-09-09 23:27:14 +00:00
|
|
|
import { BasicTests, warns } from "test/helper/Basic";
|
2019-08-12 04:16:03 +00:00
|
|
|
import { CompareToFile } from "test/helper/CompareToFile";
|
|
|
|
import { atTime, Offline } from "test/helper/Offline";
|
|
|
|
import { OutputAudio } from "test/helper/OutputAudio";
|
|
|
|
import { PolySynth } from "./PolySynth";
|
|
|
|
import { Synth } from "./Synth";
|
2019-10-07 19:01:22 +00:00
|
|
|
import { FMSynth } from "./FMSynth";
|
|
|
|
import { PluckSynth } from "./PluckSynth";
|
|
|
|
import { MetalSynth } from "./MetalSynth";
|
|
|
|
import { MembraneSynth } from "./MembraneSynth";
|
2019-08-12 04:16:03 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
2019-10-07 19:01:22 +00:00
|
|
|
it("can be constructed with monophonic synths", () => {
|
|
|
|
expect(() => {
|
|
|
|
const polySynth = new PolySynth(Synth);
|
|
|
|
polySynth.dispose();
|
|
|
|
}).to.not.throw(Error);
|
|
|
|
expect(() => {
|
|
|
|
const polySynth = new PolySynth(FMSynth);
|
|
|
|
polySynth.dispose();
|
|
|
|
}).to.not.throw(Error);
|
|
|
|
expect(() => {
|
|
|
|
const polySynth = new PolySynth(MetalSynth);
|
|
|
|
polySynth.dispose();
|
|
|
|
}).to.not.throw(Error);
|
|
|
|
expect(() => {
|
|
|
|
const polySynth = new PolySynth(MembraneSynth);
|
|
|
|
polySynth.dispose();
|
|
|
|
}).to.not.throw(Error);
|
|
|
|
});
|
|
|
|
|
2019-08-12 04:16:03 +00:00
|
|
|
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-09-14 22:12:44 +00:00
|
|
|
polySynth.set({ envelope: { release: 0.1 } });
|
2019-08-12 04:16:03 +00:00
|
|
|
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-09-14 22:12:44 +00:00
|
|
|
polySynth.set({ envelope: { release: 0.1 } });
|
2019-08-12 04:16:03 +00:00
|
|
|
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);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2021-03-21 09:40:11 +00:00
|
|
|
it("can stop all sounds scheduled to start in the future when disposed", () => {
|
|
|
|
return Offline(() => {
|
|
|
|
const polySynth = new PolySynth();
|
|
|
|
polySynth.set({ envelope: { release: 0.1 } });
|
|
|
|
polySynth.toDestination();
|
|
|
|
polySynth.triggerAttackRelease(["C4", "E4", "G4", "B4"], 0.2);
|
|
|
|
return atTime(0.1, () => {
|
|
|
|
polySynth.dispose();
|
|
|
|
});
|
|
|
|
}, 0.3).then((buffer) => {
|
|
|
|
expect(buffer.isSilent()).to.be.true;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-09-04 03:07:44 +00:00
|
|
|
it("can be synced to the transport", () => {
|
2019-09-14 22:12:44 +00:00
|
|
|
return Offline(({ transport }) => {
|
2019-09-04 03:07:44 +00:00
|
|
|
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, {
|
2019-09-14 22:12:44 +00:00
|
|
|
envelope: {
|
2019-09-04 03:07:44 +00:00
|
|
|
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", () => {
|
2019-09-09 23:27:14 +00:00
|
|
|
warns(() => {
|
|
|
|
return Offline(() => {
|
|
|
|
const polySynth = new PolySynth({
|
|
|
|
maxPolyphony: 2,
|
|
|
|
});
|
|
|
|
polySynth.toDestination();
|
|
|
|
polySynth.triggerAttack(["C4", "D4", "G4"], 0.1);
|
|
|
|
}, 0.3);
|
2019-08-12 17:21:55 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
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-09-14 22:12:44 +00:00
|
|
|
polySynth.set({ envelope: { release: 0.1 } });
|
2019-08-12 04:16:03 +00:00
|
|
|
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, () => {
|
2019-10-16 03:15:41 +00:00
|
|
|
expect(polySynth.activeVoices).to.equal(2);
|
2019-08-12 17:18:18 +00:00
|
|
|
}),
|
|
|
|
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-09-14 22:12:44 +00:00
|
|
|
envelope: {
|
2019-08-14 14:50:01 +00:00
|
|
|
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-09-14 22:12:44 +00:00
|
|
|
envelope: {
|
2019-08-14 14:50:01 +00:00
|
|
|
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-09-14 22:12:44 +00:00
|
|
|
envelope: {
|
|
|
|
sustain: 0.3,
|
2019-08-12 04:16:03 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
expect(polySynth.get().envelope.sustain).to.equal(0.3);
|
|
|
|
polySynth.dispose();
|
|
|
|
});
|
|
|
|
|
2021-10-13 19:29:23 +00:00
|
|
|
it("throws an error when used without a monophonic synth", () => {
|
|
|
|
expect(() => {
|
|
|
|
// @ts-ignore
|
|
|
|
new PolySynth(PluckSynth);
|
|
|
|
}).throws(Error)
|
|
|
|
});
|
|
|
|
|
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({
|
2019-09-14 22:12:44 +00:00
|
|
|
volume: -12,
|
2019-08-12 04:16:03 +00:00
|
|
|
});
|
|
|
|
expect(polySynth.volume.value).to.be.closeTo(-12, 0.1);
|
|
|
|
polySynth.dispose();
|
|
|
|
});
|
|
|
|
|
|
|
|
it("can get/set attributes", () => {
|
|
|
|
const polySynth = new PolySynth();
|
|
|
|
polySynth.set({
|
2019-09-14 22:12:44 +00:00
|
|
|
envelope: { decay: 3 },
|
2019-08-12 04:16:03 +00:00
|
|
|
});
|
|
|
|
expect(polySynth.get().envelope.decay).to.equal(3);
|
|
|
|
polySynth.dispose();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|