1 | import { expect } from "chai";
|
2 | import { BasicTests } from "../../test/helper/Basic.js";
|
3 | import { CompareToFile } from "../../test/helper/CompareToFile.js";
|
4 | import { InstrumentTest } from "../../test/helper/InstrumentTests.js";
|
5 | import { MonophonicTest } from "../../test/helper/MonophonicTests.js";
|
6 | import { Offline } from "../../test/helper/Offline.js";
|
7 | import { Frequency } from "../core/type/Frequency.js";
|
8 | import { Synth } from "./Synth.js";
|
9 |
|
10 | describe("Synth", () => {
|
11 | BasicTests(Synth);
|
12 | InstrumentTest(Synth, "C4");
|
13 | MonophonicTest(Synth, "C4");
|
14 |
|
15 | it("matches a file basic", () => {
|
16 | return CompareToFile(
|
17 | () => {
|
18 | const synth = new Synth().toDestination();
|
19 | synth.triggerAttackRelease("C4", 0.1, 0.05);
|
20 | },
|
21 | "synth_basic.wav",
|
22 | 0.3
|
23 | );
|
24 | });
|
25 |
|
26 | it("matches a file melody", () => {
|
27 | return CompareToFile(
|
28 | () => {
|
29 | const synth = new Synth().toDestination();
|
30 | synth.triggerAttack("C4", 0);
|
31 | synth.triggerAttack("E4", 0.1, 0.5);
|
32 | synth.triggerAttackRelease("G4", 0.5, 0.3);
|
33 | synth.triggerAttackRelease("B4", 0.5, 0.5, 0.2);
|
34 | },
|
35 | "synth_melody.wav",
|
36 | 0.3
|
37 | );
|
38 | });
|
39 |
|
40 | context("API", () => {
|
41 | it("can get and set oscillator attributes", () => {
|
42 | const simple = new Synth();
|
43 | simple.oscillator.type = "triangle";
|
44 | expect(simple.oscillator.type).to.equal("triangle");
|
45 | simple.dispose();
|
46 | });
|
47 |
|
48 | it("can get and set envelope attributes", () => {
|
49 | const simple = new Synth();
|
50 | simple.envelope.attack = 0.24;
|
51 | expect(simple.envelope.attack).to.equal(0.24);
|
52 | simple.dispose();
|
53 | });
|
54 |
|
55 | it("can be constructed with an options object", () => {
|
56 | const simple = new Synth({
|
57 | envelope: {
|
58 | sustain: 0.3,
|
59 | },
|
60 | oscillator: {
|
61 | type: "sine",
|
62 | },
|
63 | volume: -5,
|
64 | });
|
65 | expect(simple.envelope.sustain).to.equal(0.3);
|
66 | expect(simple.oscillator.type).to.equal("sine");
|
67 | expect(simple.volume.value).to.be.closeTo(-5, 0.1);
|
68 | simple.dispose();
|
69 | });
|
70 |
|
71 | it("can get/set attributes", () => {
|
72 | const simple = new Synth();
|
73 | simple.set({
|
74 | envelope: {
|
75 | decay: 0.24,
|
76 | },
|
77 | });
|
78 | expect(simple.get().envelope.decay).to.equal(0.24);
|
79 | simple.dispose();
|
80 | });
|
81 |
|
82 | it("can get does not include omited oscillator attributes", () => {
|
83 | const simple = new Synth();
|
84 | expect(simple.get().oscillator).to.not.have.key("frequency");
|
85 | expect(simple.get().oscillator).to.not.have.key("detune");
|
86 | expect(Object.keys(simple.get().oscillator)).to.include("type");
|
87 | simple.dispose();
|
88 | });
|
89 |
|
90 | it("can be trigged with a Tone.Frequency", () => {
|
91 | return Offline(() => {
|
92 | const synth = new Synth().toDestination();
|
93 | synth.triggerAttack(Frequency("C4"), 0);
|
94 | }).then((buffer) => {
|
95 | expect(buffer.isSilent()).to.be.false;
|
96 | });
|
97 | });
|
98 |
|
99 | it("is silent after triggerAttack if sustain is 0", () => {
|
100 | return Offline(() => {
|
101 | const synth = new Synth({
|
102 | envelope: {
|
103 | attack: 0.1,
|
104 | decay: 0.1,
|
105 | sustain: 0,
|
106 | },
|
107 | }).toDestination();
|
108 | synth.triggerAttack("C4", 0);
|
109 | }, 0.5).then((buffer) => {
|
110 | expect(buffer.getTimeOfLastSound()).to.be.closeTo(0.2, 0.01);
|
111 | });
|
112 | });
|
113 | });
|
114 |
|
115 | context("Transport sync", () => {
|
116 | it("is silent until the transport is started", () => {
|
117 | return Offline(({ transport }) => {
|
118 | const synth = new Synth().sync().toDestination();
|
119 | synth.triggerAttackRelease("C4", 0.5);
|
120 | transport.start(0.5);
|
121 | }, 1).then((buffer) => {
|
122 | expect(buffer.getTimeOfFirstSound()).is.closeTo(0.5, 0.1);
|
123 | });
|
124 | });
|
125 |
|
126 | it("stops when the transport is stopped", () => {
|
127 | return Offline(({ transport }) => {
|
128 | const synth = new Synth({
|
129 | envelope: {
|
130 | release: 0,
|
131 | },
|
132 | })
|
133 | .sync()
|
134 | .toDestination();
|
135 | synth.triggerAttackRelease("C4", 0.5);
|
136 | transport.start(0.5).stop(1);
|
137 | }, 1.5).then((buffer) => {
|
138 | expect(buffer.getTimeOfLastSound()).is.closeTo(1, 0.1);
|
139 | });
|
140 | });
|
141 |
|
142 | it("goes silent at the loop boundary", () => {
|
143 | return Offline(({ transport }) => {
|
144 | const synth = new Synth({
|
145 | envelope: {
|
146 | release: 0,
|
147 | },
|
148 | })
|
149 | .sync()
|
150 | .toDestination();
|
151 | synth.triggerAttackRelease("C4", 0.8, 0.5);
|
152 | transport.loopEnd = 1;
|
153 | transport.loop = true;
|
154 | transport.start();
|
155 | }, 2).then((buffer) => {
|
156 | expect(buffer.getRmsAtTime(0)).to.be.closeTo(0, 0.05);
|
157 | expect(buffer.getRmsAtTime(0.6)).to.be.closeTo(0.2, 0.05);
|
158 | expect(buffer.getRmsAtTime(1.1)).to.be.closeTo(0, 0.05);
|
159 | expect(buffer.getRmsAtTime(1.6)).to.be.closeTo(0.2, 0.05);
|
160 | });
|
161 | });
|
162 |
|
163 | it("can unsync", () => {
|
164 | return Offline(({ transport }) => {
|
165 | const synth = new Synth({
|
166 | envelope: {
|
167 | sustain: 1,
|
168 | release: 0,
|
169 | },
|
170 | })
|
171 | .sync()
|
172 | .toDestination()
|
173 | .unsync();
|
174 | synth.triggerAttackRelease("C4", 1, 0.5);
|
175 | transport.start().stop(1);
|
176 | }, 2).then((buffer) => {
|
177 | expect(buffer.getRmsAtTime(0)).to.be.closeTo(0, 0.05);
|
178 | expect(buffer.getRmsAtTime(0.6)).to.be.closeTo(0.6, 0.05);
|
179 | expect(buffer.getRmsAtTime(1.4)).to.be.closeTo(0.6, 0.05);
|
180 | expect(buffer.getRmsAtTime(1.6)).to.be.closeTo(0, 0.05);
|
181 | });
|
182 | });
|
183 | });
|
184 |
|
185 | context("Portamento", () => {
|
186 | it("can play notes with a portamento", () => {
|
187 | return Offline(() => {
|
188 | const synth = new Synth({
|
189 | portamento: 0.1,
|
190 | });
|
191 | expect(synth.portamento).to.equal(0.1);
|
192 | synth.frequency.toDestination();
|
193 | synth.triggerAttack(440, 0);
|
194 | synth.triggerAttack(880, 0.1);
|
195 | }, 0.2).then((buffer) => {
|
196 | buffer.forEach((val, time) => {
|
197 | if (time < 0.1) {
|
198 | expect(val).to.be.closeTo(440, 1);
|
199 | } else if (time < 0.2) {
|
200 | expect(val).to.within(440, 880);
|
201 | } else {
|
202 | expect(val).to.be.closeTo(880, 1);
|
203 | }
|
204 | });
|
205 | });
|
206 | });
|
207 | });
|
208 | });
|