smplr 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,6 +34,10 @@ piano.start({ note: "C4" });
34
34
 
35
35
  See demo: https://danigb.github.io/smplr/
36
36
 
37
+ `smplr` is still under development and features are considered unstable until v 1.0
38
+
39
+ Read [CHANGELOG](https://github.com/danigb/smplr/blob/main/CHANGELOG.md) for changes.
40
+
37
41
  #### Library goals
38
42
 
39
43
  - No setup: specifically, all samples are online, so no need for a server.
@@ -48,13 +52,11 @@ Install with npm or your favourite package manager:
48
52
  npm i smplr
49
53
  ```
50
54
 
51
- Samples are published at: https://github.com/danigb/samples
52
-
53
- `smplr` is still under development and features are considered unstable. See [CHANGELOG](https://github.com/danigb/smplr/blob/main/CHANGELOG.md) for changes.
55
+ Samples are stored at https://github.com/danigb/samples and there is no need to install them. Kudos to all samplers 🙌
54
56
 
55
57
  ## Documentation
56
58
 
57
- ### Create an instrument
59
+ ### Create and load an instrument
58
60
 
59
61
  All instruments follows the same pattern: `new Instrument(context, options?)`. For example:
60
62
 
@@ -68,10 +70,10 @@ const marimba = new Soundfont(context, { instrument: "marimba" });
68
70
 
69
71
  #### Wait for audio loading
70
72
 
71
- You can start playing notes as soon as one audio is loaded. But if you want to wait for all of them, you can use the `loaded()` function that returns a promise:
73
+ You can start playing notes as soon as one audio is loaded. But if you want to wait for all of them, you can use the `load` property that returns a promise:
72
74
 
73
75
  ```js
74
- piano.loaded().then(() => {
76
+ piano.load.then(() => {
75
77
  // now the piano is fully loaded
76
78
  });
77
79
  ```
@@ -79,28 +81,9 @@ piano.loaded().then(() => {
79
81
  Since the promise returns the instrument instance, you can create and wait in a single line:
80
82
 
81
83
  ```js
82
- const piano = await new SplendidGrandPiano(context).loaded();
83
- ```
84
-
85
- #### Cache requests
86
-
87
- [Experimental]
88
-
89
- If you use default samples, they are stored at github pages. Github rate limits the number of requests per second. That could be a problem, specially if you're using a development environment with hot reload (like most React frameworks).
90
-
91
- If you want to cache samples on the browser you can use a `CacheStorage` object:
92
-
93
- ```ts
94
- import { SplendidGrandPiano, CacheStorage } from "smplr";
95
-
96
- const context = new AudioContext();
97
- const storage = new CacheStorage();
98
- // First time the instrument loads, will fetch the samples from http. Subsequent times from cache.
99
- const piano = new SplendidGrandPiano(context, { storage });
84
+ const piano = await new SplendidGrandPiano(context).load;
100
85
  ```
101
86
 
102
- ⚠️ `CacheStorage` is based on [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache) and only works in secure environments that runs with `https`. Read your framework documentation for setup instructions. For example, in nextjs you can use https://www.npmjs.com/package/next-dev-https. For vite there's https://github.com/liuweiGL/vite-plugin-mkcert. Find the appropriate solution for your environment.
103
-
104
87
  ### Play
105
88
 
106
89
  #### Start and stop notes
@@ -149,23 +132,23 @@ const now = context.currentTime;
149
132
  });
150
133
  ```
151
134
 
152
- #### onEnded event
135
+ #### Looping
153
136
 
154
- You can add a `onEnded` callback that will be invoked when the note ends:
137
+ You can loop a note by using `loop`, `loopStart` and `loopEnd`:
155
138
 
156
139
  ```js
157
- piano.start({
158
- note: "C4",
159
- duration: 1,
160
- onEnded: () => {
161
- // will be called after 1 second
162
- },
140
+ const sampler = new Sampler(audioContext, { string: "mi-long-sample.mp3" });
141
+ sampler.start({
142
+ note: "string",
143
+ loop: "true",
144
+ loopStart: 1.0,
145
+ loopEnd: 9.0,
163
146
  });
164
147
  ```
165
148
 
166
- The callback will receive as parameter the same object you pass to the `start` function;
149
+ If `loopStart` or `loopEnd` is not specified it will be use by default 0 and total duration respectively.
167
150
 
168
- ### Change volume
151
+ #### Change volume
169
152
 
170
153
  Instrument `output` attribute represents the main output of the instrument. `output.setVolume` method accepts a number where 0 means no volume, and 127 is max volume without amplification:
171
154
 
@@ -194,6 +177,41 @@ To change the mix level, use `output.sendEffect(name, mix)`:
194
177
  piano.output.sendEffect("reverb", 0.5);
195
178
  ```
196
179
 
180
+ #### Events
181
+
182
+ You can add a `onEnded` callback that will be invoked when the note ends:
183
+
184
+ ```js
185
+ piano.start({
186
+ note: "C4",
187
+ duration: 1,
188
+ onEnded: () => {
189
+ // will be called after 1 second
190
+ },
191
+ });
192
+ ```
193
+
194
+ The callback will receive as parameter the same object you pass to the `start` function;
195
+
196
+ ### Experimental features
197
+
198
+ #### Cache requests
199
+
200
+ If you use default samples, they are stored at github pages. Github rate limits the number of requests per second. That could be a problem, specially if you're using a development environment with hot reload (like most React frameworks).
201
+
202
+ If you want to cache samples on the browser you can use a `CacheStorage` object:
203
+
204
+ ```ts
205
+ import { SplendidGrandPiano, CacheStorage } from "smplr";
206
+
207
+ const context = new AudioContext();
208
+ const storage = new CacheStorage();
209
+ // First time the instrument loads, will fetch the samples from http. Subsequent times from cache.
210
+ const piano = new SplendidGrandPiano(context, { storage });
211
+ ```
212
+
213
+ ⚠️ `CacheStorage` is based on [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache) and only works in secure environments that runs with `https`. Read your framework documentation for setup instructions. For example, in nextjs you can use https://www.npmjs.com/package/next-dev-https. For vite there's https://github.com/liuweiGL/vite-plugin-mkcert. Find the appropriate solution for your environment.
214
+
197
215
  ## Instruments
198
216
 
199
217
  ### Sampler
@@ -242,11 +260,24 @@ Alternatively, you can pass your custom url as the instrument. In that case, the
242
260
 
243
261
  ```js
244
262
  const marimba = new Soundfont(context, {
245
- instrument:
263
+ instrumentUrl:
246
264
  "https://gleitz.github.io/midi-js-soundfonts/MusyngKite/marimba-mp3.js",
247
265
  });
248
266
  ```
249
267
 
268
+ #### Looping (experimental)
269
+
270
+ You can enable note looping to make note names indefinitely long by loading loop data:
271
+
272
+ ```js
273
+ const marimba = new Soundfont(context, {
274
+ instrument: "cello",
275
+ loadLoopData: true,
276
+ });
277
+ ```
278
+
279
+ Bear in mind that currently that feature produces click on lot of instruments.
280
+
250
281
  ### Piano
251
282
 
252
283
  A sampled acoustic piano. It uses Steinway samples with 4 velocity layers from
@@ -299,6 +330,20 @@ const mallet = new Mallet(new AudioContext(), {
299
330
  });
300
331
  ```
301
332
 
333
+ ### Mellotron
334
+
335
+ Samples from [archive.org](https://archive.org/details/mellotron-archive-cd-rom-nki-wav.-7z)
336
+
337
+ ```js
338
+ import { Mellotron, getMellotronNames } from "smplr";
339
+
340
+ const instruments = getMellotronNames();
341
+
342
+ const mallet = new Mellotron(new AudioContext(), {
343
+ instrument: instruments[0],
344
+ });
345
+ ```
346
+
302
347
  ### Drum Machines
303
348
 
304
349
  Sampled drum machines. Samples from different sources:
package/dist/index.d.ts CHANGED
@@ -1,19 +1,3 @@
1
- type StorageResponse = {
2
- readonly status: number;
3
- arrayBuffer(): Promise<ArrayBuffer>;
4
- json(): Promise<any>;
5
- text(): Promise<string>;
6
- };
7
- type Storage = {
8
- fetch: (url: string) => Promise<StorageResponse>;
9
- };
10
- declare const HttpStorage: Storage;
11
- declare class CacheStorage implements Storage {
12
- #private;
13
- constructor(name?: string);
14
- fetch(url: string): Promise<StorageResponse>;
15
- }
16
-
17
1
  type AudioInsert = {
18
2
  input: AudioNode;
19
3
  output: AudioNode;
@@ -24,15 +8,17 @@ type ChannelOptions = {
24
8
  volume: number;
25
9
  volumeToGain: (volume: number) => number;
26
10
  };
11
+ type OutputChannel = Omit<Channel, "input">;
27
12
  /**
13
+ * An output channel with audio effects
28
14
  * @private
29
15
  */
30
16
  declare class Channel {
31
17
  #private;
32
- readonly context: AudioContext;
18
+ readonly context: BaseAudioContext;
33
19
  readonly setVolume: (vol: number) => void;
34
20
  readonly input: AudioNode;
35
- constructor(context: AudioContext, options: Partial<ChannelOptions>);
21
+ constructor(context: BaseAudioContext, options: Partial<ChannelOptions>);
36
22
  addInsert(effect: AudioNode | AudioInsert): void;
37
23
  addEffect(name: string, effect: AudioNode | {
38
24
  input: AudioNode;
@@ -41,72 +27,90 @@ declare class Channel {
41
27
  disconnect(): void;
42
28
  }
43
29
 
30
+ type StorageResponse = {
31
+ readonly status: number;
32
+ arrayBuffer(): Promise<ArrayBuffer>;
33
+ json(): Promise<any>;
34
+ text(): Promise<string>;
35
+ };
36
+ type Storage = {
37
+ fetch: (url: string) => Promise<StorageResponse>;
38
+ };
39
+ declare const HttpStorage: Storage;
40
+ declare class CacheStorage implements Storage {
41
+ #private;
42
+ constructor(name?: string);
43
+ fetch(url: string): Promise<StorageResponse>;
44
+ }
45
+
44
46
  type AudioBuffers = Record<string | number, AudioBuffer | undefined>;
47
+ /**
48
+ * A function that downloads audio into a AudioBuffers
49
+ */
50
+ type AudioBuffersLoader = (context: BaseAudioContext, buffers: AudioBuffers) => Promise<void>;
45
51
 
46
- type StopSample = {
47
- stopId?: string | number;
48
- time?: number;
49
- };
52
+ /**
53
+ * A function to unsubscribe from an event or control
54
+ */
55
+ type Unsubscribe = () => void;
56
+ /**
57
+ * A function that listener to event or control changes
58
+ */
59
+ type Listener<T> = (value: T) => void;
60
+ /**
61
+ * A function to subscribe an trigger or control events
62
+ */
63
+ type Subscribe<T> = (listener: Listener<T>) => Unsubscribe;
50
64
 
51
65
  /**
52
- * A function that downloads audio
66
+ * @private
53
67
  */
54
- type SamplerAudioLoader = (context: AudioContext, buffers: AudioBuffers) => Promise<void>;
55
- type SamplerConfig = {
56
- storage?: Storage;
57
- detune: number;
58
- volume: number;
59
- velocity: number;
60
- decayTime?: number;
61
- lpfCutoffHz?: number;
62
- destination: AudioNode;
63
- buffers: Record<string | number, string | AudioBuffers> | SamplerAudioLoader;
64
- volumeToGain: (volume: number) => number;
65
- noteToSample: (note: SamplerNote, buffers: AudioBuffers, config: SamplerConfig) => [string | number, number];
68
+ type InternalPlayer = {
69
+ readonly buffers: AudioBuffers;
70
+ readonly context: BaseAudioContext;
71
+ start(sample: SampleStart): (time?: number) => void;
72
+ stop(sample?: SampleStop): void;
73
+ disconnect(): void;
66
74
  };
67
- type SamplerNote = {
75
+ type SampleStop = {
76
+ stopId?: string | number;
77
+ time?: number;
78
+ };
79
+ type SampleOptions = {
68
80
  decayTime?: number;
69
81
  detune?: number;
70
- duration?: number;
82
+ duration?: number | null;
83
+ velocity?: number;
71
84
  lpfCutoffHz?: number;
85
+ loop?: boolean;
86
+ loopStart?: number;
87
+ loopEnd?: number;
88
+ };
89
+ type SampleStart = {
90
+ name?: string;
72
91
  note: string | number;
73
- onEnded?: (note: SamplerNote | string | number) => void;
92
+ onEnded?: (sample: SampleStart) => void;
93
+ stop?: Subscribe<number>;
74
94
  stopId?: string | number;
75
95
  time?: number;
76
- velocity?: number;
77
- };
78
- /**
79
- * A Sampler instrument
80
- *
81
- * @private
82
- */
83
- declare class Sampler {
84
- #private;
85
- readonly context: AudioContext;
86
- readonly output: Omit<Channel, "input">;
87
- readonly buffers: AudioBuffers;
88
- constructor(context: AudioContext, options?: Partial<SamplerConfig>);
89
- loaded(): Promise<this>;
90
- start(note: SamplerNote | string | number): (time?: number | undefined) => void;
91
- stop(note?: StopSample | string | number): void;
92
- }
96
+ } & SampleOptions;
93
97
 
94
98
  declare function getDrumMachineNames(): string[];
95
- type DrumMachineConfig = {
99
+ type DrumMachineConfig = ChannelOptions & SampleOptions & {
96
100
  instrument: string;
97
- destination: AudioNode;
98
101
  storage?: Storage;
99
- detune: number;
100
- volume: number;
101
- velocity: number;
102
- decayTime?: number;
103
- lpfCutoffHz?: number;
104
102
  };
105
- declare class DrumMachine extends Sampler {
103
+ declare class DrumMachine {
106
104
  #private;
105
+ private readonly player;
106
+ readonly load: Promise<this>;
107
+ readonly output: OutputChannel;
107
108
  constructor(context: AudioContext, options?: Partial<DrumMachineConfig>);
109
+ loaded(): Promise<this>;
108
110
  get sampleNames(): string[];
109
111
  getVariations(name: string): string[];
112
+ start(sample: SampleStart): (time?: number | undefined) => void;
113
+ stop(sample: SampleStop): void;
110
114
  }
111
115
 
112
116
  type SfzInstrument = {
@@ -194,12 +198,14 @@ type SfzSamplerConfig = {
194
198
  declare class SfzSampler {
195
199
  #private;
196
200
  readonly context: AudioContext;
197
- readonly output: Omit<Channel, "input">;
198
- readonly buffers: AudioBuffers;
201
+ readonly options: Readonly<Partial<SfzSamplerConfig>>;
202
+ private readonly player;
203
+ readonly load: Promise<this>;
199
204
  constructor(context: AudioContext, options: Partial<SfzSamplerConfig> & Pick<SfzSamplerConfig, "instrument">);
205
+ get output(): OutputChannel;
200
206
  loaded(): Promise<this>;
201
- start(note: SamplerNote | string | number): (time?: number) => void;
202
- stop(note?: StopSample | string | number): void;
207
+ start(sample: SampleStart | string | number): void;
208
+ stop(sample?: SampleStop | string | number): void;
203
209
  disconnect(): void;
204
210
  }
205
211
 
@@ -221,6 +227,26 @@ declare class Mallet extends SfzSampler {
221
227
  }
222
228
  declare const NAME_TO_PATH: Record<string, string | undefined>;
223
229
 
230
+ declare function getMellotronNames(): string[];
231
+ type MellotronConfig = {
232
+ instrument: string;
233
+ storage: Storage;
234
+ };
235
+ type MellotronOptions = Partial<SampleOptions & ChannelOptions & MellotronConfig>;
236
+ declare class Mellotron implements InternalPlayer {
237
+ readonly context: BaseAudioContext;
238
+ private readonly config;
239
+ private readonly player;
240
+ private readonly layer;
241
+ readonly load: Promise<this>;
242
+ constructor(context: BaseAudioContext, options: MellotronOptions);
243
+ get buffers(): AudioBuffers;
244
+ get output(): OutputChannel;
245
+ start(sample: SampleStart | string | number): (time?: number | undefined) => void;
246
+ stop(sample?: SampleStop | string | number): void;
247
+ disconnect(): void;
248
+ }
249
+
224
250
  declare const PARAMS: readonly ["preDelay", "bandwidth", "inputDiffusion1", "inputDiffusion2", "decay", "decayDiffusion1", "decayDiffusion2", "damping", "excursionRate", "excursionDepth", "wet", "dry"];
225
251
  declare class Reverb {
226
252
  #private;
@@ -233,25 +259,61 @@ declare class Reverb {
233
259
  connect(output: AudioNode): void;
234
260
  }
235
261
 
236
- type SoundfontConfig = {
237
- kit: "FluidR3_GM" | "MusyngKite" | string;
238
- instrument: string;
262
+ type SamplerConfig = {
239
263
  storage?: Storage;
240
- destination: AudioNode;
241
264
  detune: number;
242
265
  volume: number;
243
266
  velocity: number;
244
267
  decayTime?: number;
245
268
  lpfCutoffHz?: number;
246
- extraGain?: number;
269
+ destination: AudioNode;
270
+ buffers: Record<string | number, string | AudioBuffers> | AudioBuffersLoader;
271
+ volumeToGain: (volume: number) => number;
247
272
  };
248
- declare class Soundfont extends Sampler {
249
- constructor(context: AudioContext, options: Partial<SoundfontConfig> & {
250
- instrument: string;
251
- });
273
+ /**
274
+ * A Sampler instrument
275
+ *
276
+ * @private
277
+ */
278
+ declare class Sampler {
279
+ #private;
280
+ readonly context: AudioContext;
281
+ private readonly player;
282
+ readonly load: Promise<this>;
283
+ constructor(context: AudioContext, options?: Partial<SamplerConfig>);
284
+ loaded(): Promise<this>;
285
+ get output(): OutputChannel;
286
+ start(sample: SampleStart | string | number): (time?: number | undefined) => void;
287
+ stop(sample?: SampleStop | string | number): void;
288
+ disconnect(): void;
252
289
  }
290
+
253
291
  declare function getSoundfontKits(): string[];
254
292
  declare function getSoundfontNames(): string[];
293
+ type SoundfontConfig = {
294
+ kit: "FluidR3_GM" | "MusyngKite" | string;
295
+ instrument?: string;
296
+ instrumentUrl: string;
297
+ storage: Storage;
298
+ extraGain: number;
299
+ loadLoopData: boolean;
300
+ loopDataUrl?: string;
301
+ };
302
+ type SoundfontOptions = Partial<SoundfontConfig & SampleOptions & ChannelOptions>;
303
+ declare class Soundfont {
304
+ #private;
305
+ readonly context: AudioContext;
306
+ readonly config: Readonly<SoundfontConfig>;
307
+ private readonly player;
308
+ readonly load: Promise<unknown>;
309
+ constructor(context: AudioContext, options: SoundfontOptions);
310
+ get output(): OutputChannel;
311
+ loaded(): Promise<unknown>;
312
+ get hasLoops(): boolean;
313
+ disconnect(): void;
314
+ start(sample: SampleStart): (time?: number | undefined) => void;
315
+ stop(sample?: SampleStop | string | number): void;
316
+ }
255
317
 
256
318
  /**
257
319
  * Splendid Grand Piano options
@@ -259,15 +321,25 @@ declare function getSoundfontNames(): string[];
259
321
  type SplendidGrandPianoConfig = {
260
322
  baseUrl: string;
261
323
  destination: AudioNode;
262
- storage?: Storage;
324
+ storage: Storage;
263
325
  detune: number;
264
326
  volume: number;
265
327
  velocity: number;
266
328
  decayTime?: number;
267
329
  lpfCutoffHz?: number;
268
330
  };
269
- declare class SplendidGrandPiano extends Sampler {
331
+ declare class SplendidGrandPiano {
332
+ #private;
333
+ readonly context: AudioContext;
334
+ options: Readonly<SplendidGrandPianoConfig>;
335
+ private readonly player;
336
+ readonly load: Promise<this>;
270
337
  constructor(context: AudioContext, options?: Partial<SplendidGrandPianoConfig>);
338
+ get output(): OutputChannel;
339
+ get buffers(): AudioBuffers;
340
+ loaded(): Promise<this>;
341
+ start(sampleOrNote: SampleStart | number | string): (time?: number | undefined) => void;
342
+ stop(sample?: SampleStop | number | string): void;
271
343
  }
272
344
  declare const LAYERS: ({
273
345
  name: string;
@@ -281,4 +353,4 @@ declare const LAYERS: ({
281
353
  cutoff?: undefined;
282
354
  })[];
283
355
 
284
- export { CacheStorage, DrumMachine, DrumMachineConfig, ElectricPiano, HttpStorage, LAYERS, Mallet, NAME_TO_PATH, Reverb, Sampler, SamplerAudioLoader, SamplerConfig, SamplerNote, Soundfont, SoundfontConfig, SplendidGrandPiano, SplendidGrandPianoConfig, Storage, StorageResponse, getDrumMachineNames, getElectricPianoNames, getMalletNames, getSoundfontKits, getSoundfontNames };
356
+ export { CacheStorage, DrumMachine, DrumMachineConfig, ElectricPiano, HttpStorage, LAYERS, Mallet, Mellotron, MellotronConfig, MellotronOptions, NAME_TO_PATH, Reverb, Sampler, SamplerConfig, Soundfont, SoundfontOptions, SplendidGrandPiano, SplendidGrandPianoConfig, Storage, StorageResponse, getDrumMachineNames, getElectricPianoNames, getMalletNames, getMellotronNames, getSoundfontKits, getSoundfontNames };