1 | import { __decorate } from "tslib";
|
2 | import { ToneAudioBuffers } from "../core/context/ToneAudioBuffers.js";
|
3 | import { ftomf, intervalToFrequencyRatio } from "../core/type/Conversions.js";
|
4 | import { FrequencyClass } from "../core/type/Frequency.js";
|
5 | import { optionsFromArguments } from "../core/util/Defaults.js";
|
6 | import { noOp } from "../core/util/Interface.js";
|
7 | import { isArray, isNote, isNumber } from "../core/util/TypeCheck.js";
|
8 | import { Instrument } from "../instrument/Instrument.js";
|
9 | import { ToneBufferSource, } from "../source/buffer/ToneBufferSource.js";
|
10 | import { timeRange } from "../core/util/Decorator.js";
|
11 | import { assert } from "../core/util/Debug.js";
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 | export class Sampler extends Instrument {
|
34 | constructor() {
|
35 | const options = optionsFromArguments(Sampler.getDefaults(), arguments, ["urls", "onload", "baseUrl"], "urls");
|
36 | super(options);
|
37 | this.name = "Sampler";
|
38 | |
39 |
|
40 |
|
41 | this._activeSources = new Map();
|
42 | const urlMap = {};
|
43 | Object.keys(options.urls).forEach((note) => {
|
44 | const noteNumber = parseInt(note, 10);
|
45 | assert(isNote(note) || (isNumber(noteNumber) && isFinite(noteNumber)), `url key is neither a note or midi pitch: ${note}`);
|
46 | if (isNote(note)) {
|
47 |
|
48 | const mid = new FrequencyClass(this.context, note).toMidi();
|
49 | urlMap[mid] = options.urls[note];
|
50 | }
|
51 | else if (isNumber(noteNumber) && isFinite(noteNumber)) {
|
52 |
|
53 | urlMap[noteNumber] = options.urls[noteNumber];
|
54 | }
|
55 | });
|
56 | this._buffers = new ToneAudioBuffers({
|
57 | urls: urlMap,
|
58 | onload: options.onload,
|
59 | baseUrl: options.baseUrl,
|
60 | onerror: options.onerror,
|
61 | });
|
62 | this.attack = options.attack;
|
63 | this.release = options.release;
|
64 | this.curve = options.curve;
|
65 |
|
66 | if (this._buffers.loaded) {
|
67 |
|
68 | Promise.resolve().then(options.onload);
|
69 | }
|
70 | }
|
71 | static getDefaults() {
|
72 | return Object.assign(Instrument.getDefaults(), {
|
73 | attack: 0,
|
74 | baseUrl: "",
|
75 | curve: "exponential",
|
76 | onload: noOp,
|
77 | onerror: noOp,
|
78 | release: 0.1,
|
79 | urls: {},
|
80 | });
|
81 | }
|
82 | |
83 |
|
84 |
|
85 | _findClosest(midi) {
|
86 |
|
87 | const MAX_INTERVAL = 96;
|
88 | let interval = 0;
|
89 | while (interval < MAX_INTERVAL) {
|
90 |
|
91 | if (this._buffers.has(midi + interval)) {
|
92 | return -interval;
|
93 | }
|
94 | else if (this._buffers.has(midi - interval)) {
|
95 | return interval;
|
96 | }
|
97 | interval++;
|
98 | }
|
99 | throw new Error(`No available buffers for note: ${midi}`);
|
100 | }
|
101 | |
102 |
|
103 |
|
104 |
|
105 |
|
106 | triggerAttack(notes, time, velocity = 1) {
|
107 | this.log("triggerAttack", notes, time, velocity);
|
108 | if (!Array.isArray(notes)) {
|
109 | notes = [notes];
|
110 | }
|
111 | notes.forEach((note) => {
|
112 | const midiFloat = ftomf(new FrequencyClass(this.context, note).toFrequency());
|
113 | const midi = Math.round(midiFloat);
|
114 | const remainder = midiFloat - midi;
|
115 |
|
116 | const difference = this._findClosest(midi);
|
117 | const closestNote = midi - difference;
|
118 | const buffer = this._buffers.get(closestNote);
|
119 | const playbackRate = intervalToFrequencyRatio(difference + remainder);
|
120 |
|
121 | const source = new ToneBufferSource({
|
122 | url: buffer,
|
123 | context: this.context,
|
124 | curve: this.curve,
|
125 | fadeIn: this.attack,
|
126 | fadeOut: this.release,
|
127 | playbackRate,
|
128 | }).connect(this.output);
|
129 | source.start(time, 0, buffer.duration / playbackRate, velocity);
|
130 |
|
131 | if (!isArray(this._activeSources.get(midi))) {
|
132 | this._activeSources.set(midi, []);
|
133 | }
|
134 | this._activeSources.get(midi).push(source);
|
135 |
|
136 | source.onended = () => {
|
137 | if (this._activeSources && this._activeSources.has(midi)) {
|
138 | const sources = this._activeSources.get(midi);
|
139 | const index = sources.indexOf(source);
|
140 | if (index !== -1) {
|
141 | sources.splice(index, 1);
|
142 | }
|
143 | }
|
144 | };
|
145 | });
|
146 | return this;
|
147 | }
|
148 | |
149 |
|
150 |
|
151 |
|
152 | triggerRelease(notes, time) {
|
153 | this.log("triggerRelease", notes, time);
|
154 | if (!Array.isArray(notes)) {
|
155 | notes = [notes];
|
156 | }
|
157 | notes.forEach((note) => {
|
158 | const midi = new FrequencyClass(this.context, note).toMidi();
|
159 |
|
160 | if (this._activeSources.has(midi) &&
|
161 | this._activeSources.get(midi).length) {
|
162 | const sources = this._activeSources.get(midi);
|
163 | time = this.toSeconds(time);
|
164 | sources.forEach((source) => {
|
165 | source.stop(time);
|
166 | });
|
167 | this._activeSources.set(midi, []);
|
168 | }
|
169 | });
|
170 | return this;
|
171 | }
|
172 | |
173 |
|
174 |
|
175 |
|
176 | releaseAll(time) {
|
177 | const computedTime = this.toSeconds(time);
|
178 | this._activeSources.forEach((sources) => {
|
179 | while (sources.length) {
|
180 | const source = sources.shift();
|
181 | source.stop(computedTime);
|
182 | }
|
183 | });
|
184 | return this;
|
185 | }
|
186 | sync() {
|
187 | if (this._syncState()) {
|
188 | this._syncMethod("triggerAttack", 1);
|
189 | this._syncMethod("triggerRelease", 1);
|
190 | }
|
191 | return this;
|
192 | }
|
193 | |
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 | triggerAttackRelease(notes, duration, time, velocity = 1) {
|
201 | const computedTime = this.toSeconds(time);
|
202 | this.triggerAttack(notes, computedTime, velocity);
|
203 | if (isArray(duration)) {
|
204 | assert(isArray(notes), "notes must be an array when duration is array");
|
205 | notes.forEach((note, index) => {
|
206 | const d = duration[Math.min(index, duration.length - 1)];
|
207 | this.triggerRelease(note, computedTime + this.toSeconds(d));
|
208 | });
|
209 | }
|
210 | else {
|
211 | this.triggerRelease(notes, computedTime + this.toSeconds(duration));
|
212 | }
|
213 | return this;
|
214 | }
|
215 | |
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 | add(note, url, callback) {
|
222 | assert(isNote(note) || isFinite(note), `note must be a pitch or midi: ${note}`);
|
223 | if (isNote(note)) {
|
224 |
|
225 | const mid = new FrequencyClass(this.context, note).toMidi();
|
226 | this._buffers.add(mid, url, callback);
|
227 | }
|
228 | else {
|
229 |
|
230 | this._buffers.add(note, url, callback);
|
231 | }
|
232 | return this;
|
233 | }
|
234 | |
235 |
|
236 |
|
237 | get loaded() {
|
238 | return this._buffers.loaded;
|
239 | }
|
240 | |
241 |
|
242 |
|
243 | dispose() {
|
244 | super.dispose();
|
245 | this._buffers.dispose();
|
246 | this._activeSources.forEach((sources) => {
|
247 | sources.forEach((source) => source.dispose());
|
248 | });
|
249 | this._activeSources.clear();
|
250 | return this;
|
251 | }
|
252 | }
|
253 | __decorate([
|
254 | timeRange(0)
|
255 | ], Sampler.prototype, "attack", void 0);
|
256 | __decorate([
|
257 | timeRange(0)
|
258 | ], Sampler.prototype, "release", void 0);
|
259 |
|
\ | No newline at end of file |