@al8b/audio 0.1.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.
Files changed (56) hide show
  1. package/README.md +23 -0
  2. package/dist/constants.d.mts +8 -0
  3. package/dist/constants.d.ts +8 -0
  4. package/dist/constants.js +37 -0
  5. package/dist/constants.js.map +1 -0
  6. package/dist/constants.mjs +10 -0
  7. package/dist/constants.mjs.map +1 -0
  8. package/dist/core/audio-core.d.mts +98 -0
  9. package/dist/core/audio-core.d.ts +98 -0
  10. package/dist/core/audio-core.js +664 -0
  11. package/dist/core/audio-core.js.map +1 -0
  12. package/dist/core/audio-core.mjs +641 -0
  13. package/dist/core/audio-core.mjs.map +1 -0
  14. package/dist/core/audio-worklet.d.mts +3 -0
  15. package/dist/core/audio-worklet.d.ts +3 -0
  16. package/dist/core/audio-worklet.js +153 -0
  17. package/dist/core/audio-worklet.js.map +1 -0
  18. package/dist/core/audio-worklet.mjs +128 -0
  19. package/dist/core/audio-worklet.mjs.map +1 -0
  20. package/dist/core/index.d.mts +2 -0
  21. package/dist/core/index.d.ts +2 -0
  22. package/dist/core/index.js +666 -0
  23. package/dist/core/index.js.map +1 -0
  24. package/dist/core/index.mjs +641 -0
  25. package/dist/core/index.mjs.map +1 -0
  26. package/dist/devices/beeper.d.mts +21 -0
  27. package/dist/devices/beeper.d.ts +21 -0
  28. package/dist/devices/beeper.js +286 -0
  29. package/dist/devices/beeper.js.map +1 -0
  30. package/dist/devices/beeper.mjs +261 -0
  31. package/dist/devices/beeper.mjs.map +1 -0
  32. package/dist/devices/index.d.mts +3 -0
  33. package/dist/devices/index.d.ts +3 -0
  34. package/dist/devices/index.js +534 -0
  35. package/dist/devices/index.js.map +1 -0
  36. package/dist/devices/index.mjs +507 -0
  37. package/dist/devices/index.mjs.map +1 -0
  38. package/dist/devices/music.d.mts +27 -0
  39. package/dist/devices/music.d.ts +27 -0
  40. package/dist/devices/music.js +104 -0
  41. package/dist/devices/music.js.map +1 -0
  42. package/dist/devices/music.mjs +81 -0
  43. package/dist/devices/music.mjs.map +1 -0
  44. package/dist/devices/sound.d.mts +22 -0
  45. package/dist/devices/sound.d.ts +22 -0
  46. package/dist/devices/sound.js +198 -0
  47. package/dist/devices/sound.js.map +1 -0
  48. package/dist/devices/sound.mjs +175 -0
  49. package/dist/devices/sound.mjs.map +1 -0
  50. package/dist/index.d.mts +4 -0
  51. package/dist/index.d.ts +4 -0
  52. package/dist/index.js +916 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/index.mjs +888 -0
  55. package/dist/index.mjs.map +1 -0
  56. package/package.json +37 -0
@@ -0,0 +1,641 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/core/audio-core.ts
5
+ import { APIErrorCode, reportRuntimeError } from "@al8b/diagnostics";
6
+
7
+ // src/constants.ts
8
+ var A4_FREQUENCY = 440;
9
+ var SEMITONE_RATIO = 2 ** (1 / 12);
10
+ var A4_MIDI_NOTE = 69;
11
+
12
+ // src/devices/beeper.ts
13
+ var Beeper = class {
14
+ static {
15
+ __name(this, "Beeper");
16
+ }
17
+ audio;
18
+ notes = {};
19
+ plainNotes = {};
20
+ currentOctave = 5;
21
+ currentDuration = 0.5;
22
+ currentVolume = 0.5;
23
+ currentSpan = 1;
24
+ currentWaveform = "square";
25
+ constructor(audio) {
26
+ this.audio = audio;
27
+ this.initializeNotes();
28
+ }
29
+ /**
30
+ * Initialize note mappings
31
+ */
32
+ initializeNotes() {
33
+ const noteNames = [
34
+ [
35
+ "C",
36
+ "DO"
37
+ ],
38
+ [
39
+ "C#",
40
+ "DO#",
41
+ "Db",
42
+ "REb"
43
+ ],
44
+ [
45
+ "D",
46
+ "RE"
47
+ ],
48
+ [
49
+ "D#",
50
+ "RE#",
51
+ "Eb",
52
+ "MIb"
53
+ ],
54
+ [
55
+ "E",
56
+ "MI"
57
+ ],
58
+ [
59
+ "F",
60
+ "FA"
61
+ ],
62
+ [
63
+ "F#",
64
+ "FA#",
65
+ "Gb",
66
+ "SOLb"
67
+ ],
68
+ [
69
+ "G",
70
+ "SOL"
71
+ ],
72
+ [
73
+ "G#",
74
+ "SOL#",
75
+ "Ab",
76
+ "LAb"
77
+ ],
78
+ [
79
+ "A",
80
+ "LA"
81
+ ],
82
+ [
83
+ "A#",
84
+ "LA#",
85
+ "Bb",
86
+ "SIb"
87
+ ],
88
+ [
89
+ "B",
90
+ "SI"
91
+ ]
92
+ ];
93
+ for (let i = 0; i <= 127; i++) {
94
+ this.notes[i] = i;
95
+ const oct = Math.floor(i / 12) - 1;
96
+ for (const n of noteNames[i % 12]) {
97
+ this.notes[n + oct] = i;
98
+ }
99
+ if (oct === -1) {
100
+ for (const n of noteNames[i % 12]) {
101
+ this.plainNotes[n] = i;
102
+ }
103
+ }
104
+ }
105
+ }
106
+ /**
107
+ * Parse and play beep sequence
108
+ */
109
+ beep(input) {
110
+ let status = "normal";
111
+ const sequence = [];
112
+ const loops = [];
113
+ let note;
114
+ const parsed = input.split(" ");
115
+ for (const t of parsed) {
116
+ if (t === "") continue;
117
+ switch (status) {
118
+ case "normal":
119
+ if (this.notes[t] !== void 0) {
120
+ note = this.notes[t];
121
+ this.currentOctave = Math.floor(note / 12);
122
+ sequence.push({
123
+ frequency: A4_FREQUENCY * SEMITONE_RATIO ** (note - A4_MIDI_NOTE),
124
+ volume: this.currentVolume,
125
+ span: this.currentSpan,
126
+ duration: this.currentDuration,
127
+ waveform: this.currentWaveform
128
+ });
129
+ } else if (this.plainNotes[t] !== void 0) {
130
+ note = this.plainNotes[t] + this.currentOctave * 12;
131
+ sequence.push({
132
+ frequency: A4_FREQUENCY * SEMITONE_RATIO ** (note - A4_MIDI_NOTE),
133
+ volume: this.currentVolume,
134
+ span: this.currentSpan,
135
+ duration: this.currentDuration,
136
+ waveform: this.currentWaveform
137
+ });
138
+ } else if ([
139
+ "square",
140
+ "sine",
141
+ "saw",
142
+ "noise"
143
+ ].includes(t)) {
144
+ this.currentWaveform = t;
145
+ } else if ([
146
+ "tempo",
147
+ "duration",
148
+ "volume",
149
+ "span",
150
+ "loop",
151
+ "to"
152
+ ].includes(t)) {
153
+ status = t;
154
+ } else if (t === "-") {
155
+ sequence.push({
156
+ frequency: A4_FREQUENCY,
157
+ volume: 0,
158
+ span: this.currentSpan,
159
+ duration: this.currentDuration,
160
+ waveform: this.currentWaveform
161
+ });
162
+ } else if (t === "end") {
163
+ if (loops.length > 0 && sequence.length > 0) {
164
+ sequence.push({
165
+ frequency: A4_FREQUENCY,
166
+ volume: 0,
167
+ span: this.currentSpan,
168
+ duration: 0,
169
+ waveform: this.currentWaveform
170
+ });
171
+ const lop = loops.splice(loops.length - 1, 1)[0];
172
+ sequence[sequence.length - 1].loopto = lop.start;
173
+ sequence[sequence.length - 1].repeats = lop.repeats;
174
+ }
175
+ }
176
+ break;
177
+ case "tempo": {
178
+ status = "normal";
179
+ const tempo = Number.parseFloat(t);
180
+ if (!Number.isNaN(tempo) && tempo > 0) {
181
+ this.currentDuration = 60 / tempo;
182
+ }
183
+ break;
184
+ }
185
+ case "duration": {
186
+ status = "normal";
187
+ const duration = Number.parseFloat(t);
188
+ if (!Number.isNaN(duration) && duration > 0) {
189
+ this.currentDuration = duration / 1e3;
190
+ }
191
+ break;
192
+ }
193
+ case "volume": {
194
+ status = "normal";
195
+ const volume = Number.parseFloat(t);
196
+ if (!Number.isNaN(volume)) {
197
+ this.currentVolume = volume / 100;
198
+ }
199
+ break;
200
+ }
201
+ case "span": {
202
+ status = "normal";
203
+ const span = Number.parseFloat(t);
204
+ if (!Number.isNaN(span)) {
205
+ this.currentSpan = span / 100;
206
+ }
207
+ break;
208
+ }
209
+ case "loop": {
210
+ status = "normal";
211
+ loops.push({
212
+ start: sequence.length
213
+ });
214
+ const repeats = Number.parseFloat(t);
215
+ if (!Number.isNaN(repeats)) {
216
+ loops[loops.length - 1].repeats = repeats;
217
+ }
218
+ break;
219
+ }
220
+ case "to":
221
+ status = "normal";
222
+ if (note !== void 0) {
223
+ let n;
224
+ if (this.notes[t] !== void 0) {
225
+ n = this.notes[t];
226
+ } else if (this.plainNotes[t] !== void 0) {
227
+ n = this.plainNotes[t] + this.currentOctave * 12;
228
+ }
229
+ if (n !== void 0 && n !== note) {
230
+ const step = n > note ? 1 : -1;
231
+ for (let i = note + step; step > 0 ? i <= n : i >= n; i += step) {
232
+ sequence.push({
233
+ frequency: A4_FREQUENCY * SEMITONE_RATIO ** (i - A4_MIDI_NOTE),
234
+ volume: this.currentVolume,
235
+ span: this.currentSpan,
236
+ duration: this.currentDuration,
237
+ waveform: this.currentWaveform
238
+ });
239
+ }
240
+ note = n;
241
+ }
242
+ }
243
+ break;
244
+ }
245
+ }
246
+ if (loops.length > 0 && sequence.length > 0) {
247
+ const lop = loops.splice(loops.length - 1, 1)[0];
248
+ sequence.push({
249
+ frequency: A4_FREQUENCY,
250
+ volume: 0,
251
+ span: this.currentSpan,
252
+ duration: 0,
253
+ waveform: this.currentWaveform
254
+ });
255
+ sequence[sequence.length - 1].loopto = lop.start;
256
+ sequence[sequence.length - 1].repeats = lop.repeats;
257
+ }
258
+ this.audio.addBeeps(sequence);
259
+ }
260
+ };
261
+
262
+ // src/core/audio-worklet.ts
263
+ var AUDIO_WORKLET_CODE = `
264
+ class L8bAudioProcessor extends AudioWorkletProcessor {
265
+ constructor() {
266
+ super();
267
+ this.beeps = [];
268
+ this.last = 0;
269
+ this.port.onmessage = (event) => {
270
+ const data = JSON.parse(event.data);
271
+ if (data.name === "cancel_beeps") {
272
+ this.beeps = [];
273
+ } else if (data.name === "beep") {
274
+ const seq = data.sequence;
275
+ // Link sequence notes together
276
+ for (let i = 0; i < seq.length; i++) {
277
+ const note = seq[i];
278
+ if (i > 0) {
279
+ seq[i - 1].next = note;
280
+ }
281
+ // Resolve loopto index to actual note reference
282
+ if (note.loopto != null) {
283
+ note.loopto = seq[note.loopto];
284
+ }
285
+ // Initialize phase and time
286
+ note.phase = 0;
287
+ note.time = 0;
288
+ }
289
+ // Add first note to beeps queue
290
+ if (seq.length > 0) {
291
+ this.beeps.push(seq[0]);
292
+ }
293
+ }
294
+ };
295
+ }
296
+
297
+ process(inputs, outputs, parameters) {
298
+ const output = outputs[0];
299
+
300
+ for (let i = 0; i < output.length; i++) {
301
+ const channel = output[i];
302
+
303
+ if (i > 0) {
304
+ // Copy first channel to other channels
305
+ for (let j = 0; j < channel.length; j++) {
306
+ channel[j] = output[0][j];
307
+ }
308
+ } else {
309
+ // Generate audio for first channel
310
+ for (let j = 0; j < channel.length; j++) {
311
+ let sig = 0;
312
+
313
+ for (let k = this.beeps.length - 1; k >= 0; k--) {
314
+ const b = this.beeps[k];
315
+ let volume = b.volume;
316
+
317
+ if (b.time / b.duration > b.span) {
318
+ volume = 0;
319
+ }
320
+
321
+ // Generate waveform
322
+ switch (b.waveform) {
323
+ case "square":
324
+ sig += b.phase > 0.5 ? volume : -volume;
325
+ break;
326
+ case "saw":
327
+ sig += (b.phase * 2 - 1) * volume;
328
+ break;
329
+ case "noise":
330
+ sig += (Math.random() * 2 - 1) * volume;
331
+ break;
332
+ default: // sine
333
+ sig += Math.sin(b.phase * Math.PI * 2) * volume;
334
+ }
335
+
336
+ b.phase = (b.phase + b.increment) % 1;
337
+ b.time += 1;
338
+
339
+ if (b.time >= b.duration) {
340
+ b.time = 0;
341
+
342
+ if (b.loopto != null) {
343
+ if (b.repeats != null && b.repeats > 0) {
344
+ if (b.loopcount == null) {
345
+ b.loopcount = 0;
346
+ }
347
+ b.loopcount++;
348
+
349
+ if (b.loopcount >= b.repeats) {
350
+ b.loopcount = 0;
351
+ if (b.next != null) {
352
+ b.next.phase = b.phase;
353
+ this.beeps[k] = b.next;
354
+ } else {
355
+ this.beeps.splice(k, 1);
356
+ }
357
+ } else {
358
+ b.loopto.phase = b.phase;
359
+ this.beeps[k] = b.loopto;
360
+ }
361
+ } else {
362
+ b.loopto.phase = b.phase;
363
+ this.beeps[k] = b.loopto;
364
+ }
365
+ } else if (b.next != null) {
366
+ b.next.phase = b.phase;
367
+ this.beeps[k] = b.next;
368
+ } else {
369
+ this.beeps.splice(k, 1);
370
+ }
371
+ }
372
+ }
373
+
374
+ this.last = this.last * 0.9 + sig * 0.1;
375
+ channel[j] = this.last;
376
+ }
377
+ }
378
+ }
379
+
380
+ return true;
381
+ }
382
+ }
383
+
384
+ registerProcessor("l8b-audio-processor", L8bAudioProcessor);
385
+ `;
386
+
387
+ // src/core/audio-core.ts
388
+ var AudioCore = class {
389
+ static {
390
+ __name(this, "AudioCore");
391
+ }
392
+ context;
393
+ buffer = [];
394
+ playing = [];
395
+ wakeupList = [];
396
+ workletNode;
397
+ beeper;
398
+ runtime;
399
+ masterVolume = 1;
400
+ constructor(runtime) {
401
+ this.runtime = runtime;
402
+ this.getContext();
403
+ }
404
+ /**
405
+ * Check if audio context is running
406
+ */
407
+ isStarted() {
408
+ return this.context.state === "running";
409
+ }
410
+ /**
411
+ * Add item to wakeup list (for mobile audio activation)
412
+ */
413
+ addToWakeUpList(item) {
414
+ this.wakeupList.push(item);
415
+ }
416
+ interfaceCache = null;
417
+ /**
418
+ * Set master volume (0-1). Applied as a multiplier to all sound/music playback.
419
+ */
420
+ setVolume(volume) {
421
+ this.masterVolume = Math.max(0, Math.min(1, volume));
422
+ }
423
+ /**
424
+ * Get current master volume (0-1)
425
+ */
426
+ getVolume() {
427
+ return this.masterVolume;
428
+ }
429
+ /**
430
+ * Get interface for game code
431
+ */
432
+ getInterface() {
433
+ if (this.interfaceCache) {
434
+ return this.interfaceCache;
435
+ }
436
+ this.interfaceCache = {
437
+ beep: /* @__PURE__ */ __name((sequence) => this.beep(sequence), "beep"),
438
+ cancelBeeps: /* @__PURE__ */ __name(() => this.cancelBeeps(), "cancelBeeps"),
439
+ playSound: /* @__PURE__ */ __name((sound, volume, pitch, pan, loopit) => this.playSound(sound, volume, pitch, pan, loopit), "playSound"),
440
+ playMusic: /* @__PURE__ */ __name((music, volume, loopit) => this.playMusic(music, volume, loopit), "playMusic"),
441
+ setVolume: /* @__PURE__ */ __name((volume) => this.setVolume(volume), "setVolume"),
442
+ getVolume: /* @__PURE__ */ __name(() => this.getVolume(), "getVolume"),
443
+ stopAll: /* @__PURE__ */ __name(() => this.stopAll(), "stopAll")
444
+ };
445
+ return this.interfaceCache;
446
+ }
447
+ /**
448
+ * Play sound effect
449
+ */
450
+ playSound(sound, volume = 1, pitch = 1, pan = 0, loopit = false) {
451
+ if (typeof sound === "string") {
452
+ const soundName = sound.replace(/\//g, "-");
453
+ const s = this.runtime.sounds[soundName];
454
+ if (!s) {
455
+ reportRuntimeError(this.runtime?.listener, APIErrorCode.E7013, {
456
+ soundName
457
+ });
458
+ return 0;
459
+ }
460
+ return s.play(volume * this.masterVolume, pitch, pan, loopit);
461
+ }
462
+ return 0;
463
+ }
464
+ /**
465
+ * Play music
466
+ */
467
+ playMusic(music, volume = 1, loopit = false) {
468
+ if (typeof music === "string") {
469
+ const musicName = music.replace(/\//g, "-");
470
+ const m = this.runtime.music[musicName];
471
+ if (!m) {
472
+ reportRuntimeError(this.runtime?.listener, APIErrorCode.E7014, {
473
+ musicName
474
+ });
475
+ return 0;
476
+ }
477
+ return m.play(volume * this.masterVolume, loopit);
478
+ }
479
+ return 0;
480
+ }
481
+ /**
482
+ * Get or create audio context (lazy initialization - created on first use)
483
+ * Note: Browser may suspend context until user interaction, which is handled automatically
484
+ */
485
+ getContext() {
486
+ if (!this.context) {
487
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
488
+ this.context = new AudioContextClass();
489
+ if (this.context.state !== "running") {
490
+ const activate = /* @__PURE__ */ __name(() => {
491
+ if (this.context && this.context.state !== "running") {
492
+ this.context.resume();
493
+ if (this.beeper) {
494
+ this.start();
495
+ }
496
+ for (const item of this.wakeupList) {
497
+ item.wakeUp();
498
+ }
499
+ document.body.removeEventListener("touchend", activate);
500
+ document.body.removeEventListener("mouseup", activate);
501
+ document.body.removeEventListener("click", activate);
502
+ document.body.removeEventListener("keydown", activate);
503
+ }
504
+ }, "activate");
505
+ document.body.addEventListener("touchend", activate, {
506
+ once: true
507
+ });
508
+ document.body.addEventListener("mouseup", activate, {
509
+ once: true
510
+ });
511
+ document.body.addEventListener("click", activate, {
512
+ once: true
513
+ });
514
+ document.body.addEventListener("keydown", activate, {
515
+ once: true
516
+ });
517
+ } else if (this.beeper) {
518
+ this.start();
519
+ }
520
+ }
521
+ return this.context;
522
+ }
523
+ /**
524
+ * Start audio processor
525
+ */
526
+ async start() {
527
+ if (this.workletNode) return;
528
+ try {
529
+ const blob = new Blob([
530
+ AUDIO_WORKLET_CODE
531
+ ], {
532
+ type: "application/javascript"
533
+ });
534
+ const url = URL.createObjectURL(blob);
535
+ await this.context.audioWorklet.addModule(url);
536
+ this.workletNode = new AudioWorkletNode(this.context, "l8b-audio-processor");
537
+ this.workletNode.connect(this.context.destination);
538
+ this.flushBuffer();
539
+ } catch (e) {
540
+ reportRuntimeError(this.runtime?.listener, APIErrorCode.E7012, {
541
+ error: String(e)
542
+ });
543
+ }
544
+ }
545
+ /**
546
+ * Flush buffered messages
547
+ */
548
+ flushBuffer() {
549
+ if (!this.workletNode) return;
550
+ while (this.buffer.length > 0) {
551
+ this.workletNode.port.postMessage(this.buffer.splice(0, 1)[0]);
552
+ }
553
+ }
554
+ /**
555
+ * Get or create beeper
556
+ */
557
+ getBeeper() {
558
+ if (!this.beeper) {
559
+ this.beeper = new Beeper(this);
560
+ if (this.context.state === "running") {
561
+ this.start();
562
+ }
563
+ }
564
+ return this.beeper;
565
+ }
566
+ /**
567
+ * Play beep sequence
568
+ */
569
+ beep(sequence) {
570
+ this.getBeeper().beep(sequence);
571
+ }
572
+ /**
573
+ * Add beeps to audio processor
574
+ */
575
+ addBeeps(beeps) {
576
+ for (const b of beeps) {
577
+ b.duration *= this.context.sampleRate;
578
+ b.increment = b.frequency / this.context.sampleRate;
579
+ }
580
+ if (this.workletNode) {
581
+ this.workletNode.port.postMessage(JSON.stringify({
582
+ name: "beep",
583
+ sequence: beeps
584
+ }));
585
+ } else {
586
+ this.buffer.push(JSON.stringify({
587
+ name: "beep",
588
+ sequence: beeps
589
+ }));
590
+ }
591
+ }
592
+ /**
593
+ * Cancel all beeps
594
+ */
595
+ cancelBeeps() {
596
+ if (this.workletNode) {
597
+ this.workletNode.port.postMessage(JSON.stringify({
598
+ name: "cancel_beeps"
599
+ }));
600
+ } else {
601
+ this.buffer.push(JSON.stringify({
602
+ name: "cancel_beeps"
603
+ }));
604
+ }
605
+ this.stopAll();
606
+ }
607
+ /**
608
+ * Add playing sound/music to list
609
+ */
610
+ addPlaying(item) {
611
+ this.playing.push(item);
612
+ }
613
+ /**
614
+ * Remove playing sound/music from list
615
+ */
616
+ removePlaying(item) {
617
+ const index = this.playing.indexOf(item);
618
+ if (index >= 0) {
619
+ this.playing.splice(index, 1);
620
+ }
621
+ }
622
+ /**
623
+ * Stop all playing sounds/music
624
+ */
625
+ stopAll() {
626
+ for (const p of this.playing) {
627
+ try {
628
+ p.stop();
629
+ } catch (err) {
630
+ reportRuntimeError(this.runtime?.listener, APIErrorCode.E7016, {
631
+ error: String(err)
632
+ });
633
+ }
634
+ }
635
+ this.playing = [];
636
+ }
637
+ };
638
+ export {
639
+ AudioCore
640
+ };
641
+ //# sourceMappingURL=index.mjs.map