@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
package/dist/index.mjs ADDED
@@ -0,0 +1,888 @@
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
+
639
+ // src/devices/music.ts
640
+ var Music = class {
641
+ static {
642
+ __name(this, "Music");
643
+ }
644
+ ready = 1;
645
+ name = "";
646
+ url;
647
+ tag;
648
+ playing = false;
649
+ audio;
650
+ constructor(audio, url) {
651
+ this.audio = audio;
652
+ this.url = url;
653
+ this.tag = new Audio(this.url);
654
+ this.ready = 1;
655
+ }
656
+ /**
657
+ * Play music with volume and loop (HTML5 Audio implementation)
658
+ */
659
+ play(volume = 1, loopit = false) {
660
+ this.playing = true;
661
+ this.tag.loop = !!loopit;
662
+ this.tag.volume = Math.max(0, Math.min(1, volume));
663
+ if (this.audio.isStarted()) {
664
+ this.tag.play();
665
+ } else {
666
+ this.audio.addToWakeUpList(this);
667
+ }
668
+ this.audio.addPlaying(this);
669
+ return {
670
+ play: /* @__PURE__ */ __name(() => {
671
+ return this.tag.play();
672
+ }, "play"),
673
+ stop: /* @__PURE__ */ __name(() => {
674
+ this.playing = false;
675
+ this.tag.pause();
676
+ this.audio.removePlaying(this);
677
+ }, "stop"),
678
+ setVolume: /* @__PURE__ */ __name((v) => {
679
+ this.tag.volume = Math.max(0, Math.min(1, v));
680
+ }, "setVolume"),
681
+ getPosition: /* @__PURE__ */ __name(() => {
682
+ return this.tag.currentTime;
683
+ }, "getPosition"),
684
+ getDuration: /* @__PURE__ */ __name(() => {
685
+ return this.tag.duration;
686
+ }, "getDuration"),
687
+ setPosition: /* @__PURE__ */ __name((pos) => {
688
+ this.tag.pause();
689
+ this.tag.currentTime = Math.max(0, Math.min(this.tag.duration, pos));
690
+ if (this.playing) {
691
+ this.tag.play();
692
+ }
693
+ }, "setPosition")
694
+ };
695
+ }
696
+ /**
697
+ * Wake up audio (for autoplay policy)
698
+ */
699
+ wakeUp() {
700
+ if (this.playing) {
701
+ this.tag.play();
702
+ }
703
+ }
704
+ /**
705
+ * Stop music
706
+ */
707
+ stop() {
708
+ this.playing = false;
709
+ this.tag.pause();
710
+ this.audio.removePlaying(this);
711
+ }
712
+ };
713
+
714
+ // src/devices/sound.ts
715
+ import { APIErrorCode as APIErrorCode2, reportRuntimeError as reportRuntimeError2 } from "@al8b/diagnostics";
716
+ var Sound = class _Sound {
717
+ static {
718
+ __name(this, "Sound");
719
+ }
720
+ ready = 0;
721
+ buffer;
722
+ name = "";
723
+ url;
724
+ audio;
725
+ constructor(audio, url) {
726
+ this.audio = audio;
727
+ if (url instanceof AudioBuffer) {
728
+ this.buffer = url;
729
+ this.url = "";
730
+ this.ready = 1;
731
+ } else {
732
+ this.url = url;
733
+ this.loadSound(url);
734
+ }
735
+ }
736
+ /**
737
+ * Load sound from URL
738
+ */
739
+ loadSound(url) {
740
+ const request = new XMLHttpRequest();
741
+ request.open("GET", url, true);
742
+ request.responseType = "arraybuffer";
743
+ request.onload = () => {
744
+ this.audio.context.decodeAudioData(request.response, (buffer) => {
745
+ this.buffer = buffer;
746
+ this.ready = 1;
747
+ }, (err) => {
748
+ reportRuntimeError2(this.audio?.runtime?.listener, APIErrorCode2.E7016, {
749
+ error: `Audio decoding failed: ${String(err)}`
750
+ });
751
+ });
752
+ };
753
+ request.onerror = () => {
754
+ reportRuntimeError2(this.audio?.runtime?.listener, APIErrorCode2.E7016, {
755
+ error: `Failed to load sound: ${url}`
756
+ });
757
+ };
758
+ request.send();
759
+ }
760
+ /**
761
+ * Play sound with volume, pitch, pan, and loop
762
+ */
763
+ play(volume = 1, pitch = 1, pan = 0, loopit = false) {
764
+ if (!this.buffer) return {
765
+ stop: /* @__PURE__ */ __name(() => {
766
+ }, "stop"),
767
+ finished: true
768
+ };
769
+ const context = this.audio.context;
770
+ const source = context.createBufferSource();
771
+ source.playbackRate.value = pitch;
772
+ source.buffer = this.buffer;
773
+ if (loopit) source.loop = true;
774
+ const gain = context.createGain();
775
+ gain.gain.value = volume;
776
+ const panner = context.createPanner();
777
+ panner.panningModel = "equalpower";
778
+ panner.setPan = (p) => {
779
+ panner.setPosition(p, 0, 1 - Math.abs(p));
780
+ };
781
+ panner.setPan(pan);
782
+ source.connect(gain);
783
+ gain.connect(panner);
784
+ panner.connect(context.destination);
785
+ source.start();
786
+ let playing = null;
787
+ if (loopit) {
788
+ playing = {
789
+ stop: /* @__PURE__ */ __name(() => {
790
+ source.stop();
791
+ }, "stop")
792
+ };
793
+ this.audio.addPlaying(playing);
794
+ }
795
+ const res = {
796
+ stop: /* @__PURE__ */ __name(() => {
797
+ source.stop();
798
+ if (playing) this.audio.removePlaying(playing);
799
+ return 1;
800
+ }, "stop"),
801
+ setVolume: /* @__PURE__ */ __name((v) => {
802
+ gain.gain.value = Math.max(0, Math.min(1, v));
803
+ }, "setVolume"),
804
+ setPitch: /* @__PURE__ */ __name((p) => {
805
+ source.playbackRate.value = Math.max(1e-3, Math.min(1e3, p));
806
+ }, "setPitch"),
807
+ setPan: /* @__PURE__ */ __name((p) => {
808
+ panner.setPan(Math.max(-1, Math.min(1, p)));
809
+ }, "setPan"),
810
+ getDuration: /* @__PURE__ */ __name(() => {
811
+ return source.buffer ? source.buffer.duration : 0;
812
+ }, "getDuration"),
813
+ finished: false
814
+ };
815
+ source.onended = () => {
816
+ res.finished = true;
817
+ };
818
+ return res;
819
+ }
820
+ /**
821
+ * Create MicroSound class for procedural sound generation
822
+ */
823
+ static createSoundClass(audiocore) {
824
+ return class MicroSound {
825
+ static {
826
+ __name(this, "MicroSound");
827
+ }
828
+ static classname = "Sound";
829
+ channels;
830
+ length;
831
+ sampleRate;
832
+ sound;
833
+ buffer;
834
+ constructor(channels, length, sampleRate = 44100) {
835
+ channels = channels === 1 ? 1 : 2;
836
+ if (!(length > 1 && length < 44100 * 1e3)) {
837
+ length = 44100;
838
+ }
839
+ if (!(sampleRate >= 8e3 && sampleRate <= 96e3)) {
840
+ sampleRate = 44100;
841
+ }
842
+ this.channels = channels;
843
+ this.length = length;
844
+ this.sampleRate = sampleRate;
845
+ this.buffer = audiocore.context.createBuffer(channels, length, sampleRate);
846
+ this.sound = new _Sound(audiocore, this.buffer);
847
+ }
848
+ /**
849
+ * Play the sound
850
+ */
851
+ play(volume, pitch, pan, loopit) {
852
+ return this.sound.play(volume, pitch, pan, loopit);
853
+ }
854
+ /**
855
+ * Write sample value to buffer
856
+ */
857
+ write(channel, position, value) {
858
+ if (channel === 0) {
859
+ const ch1 = this.buffer.getChannelData(0);
860
+ ch1[position] = value;
861
+ } else if (this.channels === 2) {
862
+ const ch2 = this.buffer.getChannelData(1);
863
+ ch2[position] = value;
864
+ }
865
+ }
866
+ /**
867
+ * Read sample value from buffer
868
+ */
869
+ read(channel, position) {
870
+ if (channel === 0) {
871
+ const ch1 = this.buffer.getChannelData(0);
872
+ return ch1[position];
873
+ } else if (this.channels === 2) {
874
+ const ch2 = this.buffer.getChannelData(1);
875
+ return ch2[position];
876
+ }
877
+ return 0;
878
+ }
879
+ };
880
+ }
881
+ };
882
+ export {
883
+ AudioCore,
884
+ Beeper,
885
+ Music,
886
+ Sound
887
+ };
888
+ //# sourceMappingURL=index.mjs.map