@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.js ADDED
@@ -0,0 +1,916 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ AudioCore: () => AudioCore,
25
+ Beeper: () => Beeper,
26
+ Music: () => Music,
27
+ Sound: () => Sound
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/core/audio-core.ts
32
+ var import_diagnostics = require("@al8b/diagnostics");
33
+
34
+ // src/constants.ts
35
+ var A4_FREQUENCY = 440;
36
+ var SEMITONE_RATIO = 2 ** (1 / 12);
37
+ var A4_MIDI_NOTE = 69;
38
+
39
+ // src/devices/beeper.ts
40
+ var Beeper = class {
41
+ static {
42
+ __name(this, "Beeper");
43
+ }
44
+ audio;
45
+ notes = {};
46
+ plainNotes = {};
47
+ currentOctave = 5;
48
+ currentDuration = 0.5;
49
+ currentVolume = 0.5;
50
+ currentSpan = 1;
51
+ currentWaveform = "square";
52
+ constructor(audio) {
53
+ this.audio = audio;
54
+ this.initializeNotes();
55
+ }
56
+ /**
57
+ * Initialize note mappings
58
+ */
59
+ initializeNotes() {
60
+ const noteNames = [
61
+ [
62
+ "C",
63
+ "DO"
64
+ ],
65
+ [
66
+ "C#",
67
+ "DO#",
68
+ "Db",
69
+ "REb"
70
+ ],
71
+ [
72
+ "D",
73
+ "RE"
74
+ ],
75
+ [
76
+ "D#",
77
+ "RE#",
78
+ "Eb",
79
+ "MIb"
80
+ ],
81
+ [
82
+ "E",
83
+ "MI"
84
+ ],
85
+ [
86
+ "F",
87
+ "FA"
88
+ ],
89
+ [
90
+ "F#",
91
+ "FA#",
92
+ "Gb",
93
+ "SOLb"
94
+ ],
95
+ [
96
+ "G",
97
+ "SOL"
98
+ ],
99
+ [
100
+ "G#",
101
+ "SOL#",
102
+ "Ab",
103
+ "LAb"
104
+ ],
105
+ [
106
+ "A",
107
+ "LA"
108
+ ],
109
+ [
110
+ "A#",
111
+ "LA#",
112
+ "Bb",
113
+ "SIb"
114
+ ],
115
+ [
116
+ "B",
117
+ "SI"
118
+ ]
119
+ ];
120
+ for (let i = 0; i <= 127; i++) {
121
+ this.notes[i] = i;
122
+ const oct = Math.floor(i / 12) - 1;
123
+ for (const n of noteNames[i % 12]) {
124
+ this.notes[n + oct] = i;
125
+ }
126
+ if (oct === -1) {
127
+ for (const n of noteNames[i % 12]) {
128
+ this.plainNotes[n] = i;
129
+ }
130
+ }
131
+ }
132
+ }
133
+ /**
134
+ * Parse and play beep sequence
135
+ */
136
+ beep(input) {
137
+ let status = "normal";
138
+ const sequence = [];
139
+ const loops = [];
140
+ let note;
141
+ const parsed = input.split(" ");
142
+ for (const t of parsed) {
143
+ if (t === "") continue;
144
+ switch (status) {
145
+ case "normal":
146
+ if (this.notes[t] !== void 0) {
147
+ note = this.notes[t];
148
+ this.currentOctave = Math.floor(note / 12);
149
+ sequence.push({
150
+ frequency: A4_FREQUENCY * SEMITONE_RATIO ** (note - A4_MIDI_NOTE),
151
+ volume: this.currentVolume,
152
+ span: this.currentSpan,
153
+ duration: this.currentDuration,
154
+ waveform: this.currentWaveform
155
+ });
156
+ } else if (this.plainNotes[t] !== void 0) {
157
+ note = this.plainNotes[t] + this.currentOctave * 12;
158
+ sequence.push({
159
+ frequency: A4_FREQUENCY * SEMITONE_RATIO ** (note - A4_MIDI_NOTE),
160
+ volume: this.currentVolume,
161
+ span: this.currentSpan,
162
+ duration: this.currentDuration,
163
+ waveform: this.currentWaveform
164
+ });
165
+ } else if ([
166
+ "square",
167
+ "sine",
168
+ "saw",
169
+ "noise"
170
+ ].includes(t)) {
171
+ this.currentWaveform = t;
172
+ } else if ([
173
+ "tempo",
174
+ "duration",
175
+ "volume",
176
+ "span",
177
+ "loop",
178
+ "to"
179
+ ].includes(t)) {
180
+ status = t;
181
+ } else if (t === "-") {
182
+ sequence.push({
183
+ frequency: A4_FREQUENCY,
184
+ volume: 0,
185
+ span: this.currentSpan,
186
+ duration: this.currentDuration,
187
+ waveform: this.currentWaveform
188
+ });
189
+ } else if (t === "end") {
190
+ if (loops.length > 0 && sequence.length > 0) {
191
+ sequence.push({
192
+ frequency: A4_FREQUENCY,
193
+ volume: 0,
194
+ span: this.currentSpan,
195
+ duration: 0,
196
+ waveform: this.currentWaveform
197
+ });
198
+ const lop = loops.splice(loops.length - 1, 1)[0];
199
+ sequence[sequence.length - 1].loopto = lop.start;
200
+ sequence[sequence.length - 1].repeats = lop.repeats;
201
+ }
202
+ }
203
+ break;
204
+ case "tempo": {
205
+ status = "normal";
206
+ const tempo = Number.parseFloat(t);
207
+ if (!Number.isNaN(tempo) && tempo > 0) {
208
+ this.currentDuration = 60 / tempo;
209
+ }
210
+ break;
211
+ }
212
+ case "duration": {
213
+ status = "normal";
214
+ const duration = Number.parseFloat(t);
215
+ if (!Number.isNaN(duration) && duration > 0) {
216
+ this.currentDuration = duration / 1e3;
217
+ }
218
+ break;
219
+ }
220
+ case "volume": {
221
+ status = "normal";
222
+ const volume = Number.parseFloat(t);
223
+ if (!Number.isNaN(volume)) {
224
+ this.currentVolume = volume / 100;
225
+ }
226
+ break;
227
+ }
228
+ case "span": {
229
+ status = "normal";
230
+ const span = Number.parseFloat(t);
231
+ if (!Number.isNaN(span)) {
232
+ this.currentSpan = span / 100;
233
+ }
234
+ break;
235
+ }
236
+ case "loop": {
237
+ status = "normal";
238
+ loops.push({
239
+ start: sequence.length
240
+ });
241
+ const repeats = Number.parseFloat(t);
242
+ if (!Number.isNaN(repeats)) {
243
+ loops[loops.length - 1].repeats = repeats;
244
+ }
245
+ break;
246
+ }
247
+ case "to":
248
+ status = "normal";
249
+ if (note !== void 0) {
250
+ let n;
251
+ if (this.notes[t] !== void 0) {
252
+ n = this.notes[t];
253
+ } else if (this.plainNotes[t] !== void 0) {
254
+ n = this.plainNotes[t] + this.currentOctave * 12;
255
+ }
256
+ if (n !== void 0 && n !== note) {
257
+ const step = n > note ? 1 : -1;
258
+ for (let i = note + step; step > 0 ? i <= n : i >= n; i += step) {
259
+ sequence.push({
260
+ frequency: A4_FREQUENCY * SEMITONE_RATIO ** (i - A4_MIDI_NOTE),
261
+ volume: this.currentVolume,
262
+ span: this.currentSpan,
263
+ duration: this.currentDuration,
264
+ waveform: this.currentWaveform
265
+ });
266
+ }
267
+ note = n;
268
+ }
269
+ }
270
+ break;
271
+ }
272
+ }
273
+ if (loops.length > 0 && sequence.length > 0) {
274
+ const lop = loops.splice(loops.length - 1, 1)[0];
275
+ sequence.push({
276
+ frequency: A4_FREQUENCY,
277
+ volume: 0,
278
+ span: this.currentSpan,
279
+ duration: 0,
280
+ waveform: this.currentWaveform
281
+ });
282
+ sequence[sequence.length - 1].loopto = lop.start;
283
+ sequence[sequence.length - 1].repeats = lop.repeats;
284
+ }
285
+ this.audio.addBeeps(sequence);
286
+ }
287
+ };
288
+
289
+ // src/core/audio-worklet.ts
290
+ var AUDIO_WORKLET_CODE = `
291
+ class L8bAudioProcessor extends AudioWorkletProcessor {
292
+ constructor() {
293
+ super();
294
+ this.beeps = [];
295
+ this.last = 0;
296
+ this.port.onmessage = (event) => {
297
+ const data = JSON.parse(event.data);
298
+ if (data.name === "cancel_beeps") {
299
+ this.beeps = [];
300
+ } else if (data.name === "beep") {
301
+ const seq = data.sequence;
302
+ // Link sequence notes together
303
+ for (let i = 0; i < seq.length; i++) {
304
+ const note = seq[i];
305
+ if (i > 0) {
306
+ seq[i - 1].next = note;
307
+ }
308
+ // Resolve loopto index to actual note reference
309
+ if (note.loopto != null) {
310
+ note.loopto = seq[note.loopto];
311
+ }
312
+ // Initialize phase and time
313
+ note.phase = 0;
314
+ note.time = 0;
315
+ }
316
+ // Add first note to beeps queue
317
+ if (seq.length > 0) {
318
+ this.beeps.push(seq[0]);
319
+ }
320
+ }
321
+ };
322
+ }
323
+
324
+ process(inputs, outputs, parameters) {
325
+ const output = outputs[0];
326
+
327
+ for (let i = 0; i < output.length; i++) {
328
+ const channel = output[i];
329
+
330
+ if (i > 0) {
331
+ // Copy first channel to other channels
332
+ for (let j = 0; j < channel.length; j++) {
333
+ channel[j] = output[0][j];
334
+ }
335
+ } else {
336
+ // Generate audio for first channel
337
+ for (let j = 0; j < channel.length; j++) {
338
+ let sig = 0;
339
+
340
+ for (let k = this.beeps.length - 1; k >= 0; k--) {
341
+ const b = this.beeps[k];
342
+ let volume = b.volume;
343
+
344
+ if (b.time / b.duration > b.span) {
345
+ volume = 0;
346
+ }
347
+
348
+ // Generate waveform
349
+ switch (b.waveform) {
350
+ case "square":
351
+ sig += b.phase > 0.5 ? volume : -volume;
352
+ break;
353
+ case "saw":
354
+ sig += (b.phase * 2 - 1) * volume;
355
+ break;
356
+ case "noise":
357
+ sig += (Math.random() * 2 - 1) * volume;
358
+ break;
359
+ default: // sine
360
+ sig += Math.sin(b.phase * Math.PI * 2) * volume;
361
+ }
362
+
363
+ b.phase = (b.phase + b.increment) % 1;
364
+ b.time += 1;
365
+
366
+ if (b.time >= b.duration) {
367
+ b.time = 0;
368
+
369
+ if (b.loopto != null) {
370
+ if (b.repeats != null && b.repeats > 0) {
371
+ if (b.loopcount == null) {
372
+ b.loopcount = 0;
373
+ }
374
+ b.loopcount++;
375
+
376
+ if (b.loopcount >= b.repeats) {
377
+ b.loopcount = 0;
378
+ if (b.next != null) {
379
+ b.next.phase = b.phase;
380
+ this.beeps[k] = b.next;
381
+ } else {
382
+ this.beeps.splice(k, 1);
383
+ }
384
+ } else {
385
+ b.loopto.phase = b.phase;
386
+ this.beeps[k] = b.loopto;
387
+ }
388
+ } else {
389
+ b.loopto.phase = b.phase;
390
+ this.beeps[k] = b.loopto;
391
+ }
392
+ } else if (b.next != null) {
393
+ b.next.phase = b.phase;
394
+ this.beeps[k] = b.next;
395
+ } else {
396
+ this.beeps.splice(k, 1);
397
+ }
398
+ }
399
+ }
400
+
401
+ this.last = this.last * 0.9 + sig * 0.1;
402
+ channel[j] = this.last;
403
+ }
404
+ }
405
+ }
406
+
407
+ return true;
408
+ }
409
+ }
410
+
411
+ registerProcessor("l8b-audio-processor", L8bAudioProcessor);
412
+ `;
413
+
414
+ // src/core/audio-core.ts
415
+ var AudioCore = class {
416
+ static {
417
+ __name(this, "AudioCore");
418
+ }
419
+ context;
420
+ buffer = [];
421
+ playing = [];
422
+ wakeupList = [];
423
+ workletNode;
424
+ beeper;
425
+ runtime;
426
+ masterVolume = 1;
427
+ constructor(runtime) {
428
+ this.runtime = runtime;
429
+ this.getContext();
430
+ }
431
+ /**
432
+ * Check if audio context is running
433
+ */
434
+ isStarted() {
435
+ return this.context.state === "running";
436
+ }
437
+ /**
438
+ * Add item to wakeup list (for mobile audio activation)
439
+ */
440
+ addToWakeUpList(item) {
441
+ this.wakeupList.push(item);
442
+ }
443
+ interfaceCache = null;
444
+ /**
445
+ * Set master volume (0-1). Applied as a multiplier to all sound/music playback.
446
+ */
447
+ setVolume(volume) {
448
+ this.masterVolume = Math.max(0, Math.min(1, volume));
449
+ }
450
+ /**
451
+ * Get current master volume (0-1)
452
+ */
453
+ getVolume() {
454
+ return this.masterVolume;
455
+ }
456
+ /**
457
+ * Get interface for game code
458
+ */
459
+ getInterface() {
460
+ if (this.interfaceCache) {
461
+ return this.interfaceCache;
462
+ }
463
+ this.interfaceCache = {
464
+ beep: /* @__PURE__ */ __name((sequence) => this.beep(sequence), "beep"),
465
+ cancelBeeps: /* @__PURE__ */ __name(() => this.cancelBeeps(), "cancelBeeps"),
466
+ playSound: /* @__PURE__ */ __name((sound, volume, pitch, pan, loopit) => this.playSound(sound, volume, pitch, pan, loopit), "playSound"),
467
+ playMusic: /* @__PURE__ */ __name((music, volume, loopit) => this.playMusic(music, volume, loopit), "playMusic"),
468
+ setVolume: /* @__PURE__ */ __name((volume) => this.setVolume(volume), "setVolume"),
469
+ getVolume: /* @__PURE__ */ __name(() => this.getVolume(), "getVolume"),
470
+ stopAll: /* @__PURE__ */ __name(() => this.stopAll(), "stopAll")
471
+ };
472
+ return this.interfaceCache;
473
+ }
474
+ /**
475
+ * Play sound effect
476
+ */
477
+ playSound(sound, volume = 1, pitch = 1, pan = 0, loopit = false) {
478
+ if (typeof sound === "string") {
479
+ const soundName = sound.replace(/\//g, "-");
480
+ const s = this.runtime.sounds[soundName];
481
+ if (!s) {
482
+ (0, import_diagnostics.reportRuntimeError)(this.runtime?.listener, import_diagnostics.APIErrorCode.E7013, {
483
+ soundName
484
+ });
485
+ return 0;
486
+ }
487
+ return s.play(volume * this.masterVolume, pitch, pan, loopit);
488
+ }
489
+ return 0;
490
+ }
491
+ /**
492
+ * Play music
493
+ */
494
+ playMusic(music, volume = 1, loopit = false) {
495
+ if (typeof music === "string") {
496
+ const musicName = music.replace(/\//g, "-");
497
+ const m = this.runtime.music[musicName];
498
+ if (!m) {
499
+ (0, import_diagnostics.reportRuntimeError)(this.runtime?.listener, import_diagnostics.APIErrorCode.E7014, {
500
+ musicName
501
+ });
502
+ return 0;
503
+ }
504
+ return m.play(volume * this.masterVolume, loopit);
505
+ }
506
+ return 0;
507
+ }
508
+ /**
509
+ * Get or create audio context (lazy initialization - created on first use)
510
+ * Note: Browser may suspend context until user interaction, which is handled automatically
511
+ */
512
+ getContext() {
513
+ if (!this.context) {
514
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
515
+ this.context = new AudioContextClass();
516
+ if (this.context.state !== "running") {
517
+ const activate = /* @__PURE__ */ __name(() => {
518
+ if (this.context && this.context.state !== "running") {
519
+ this.context.resume();
520
+ if (this.beeper) {
521
+ this.start();
522
+ }
523
+ for (const item of this.wakeupList) {
524
+ item.wakeUp();
525
+ }
526
+ document.body.removeEventListener("touchend", activate);
527
+ document.body.removeEventListener("mouseup", activate);
528
+ document.body.removeEventListener("click", activate);
529
+ document.body.removeEventListener("keydown", activate);
530
+ }
531
+ }, "activate");
532
+ document.body.addEventListener("touchend", activate, {
533
+ once: true
534
+ });
535
+ document.body.addEventListener("mouseup", activate, {
536
+ once: true
537
+ });
538
+ document.body.addEventListener("click", activate, {
539
+ once: true
540
+ });
541
+ document.body.addEventListener("keydown", activate, {
542
+ once: true
543
+ });
544
+ } else if (this.beeper) {
545
+ this.start();
546
+ }
547
+ }
548
+ return this.context;
549
+ }
550
+ /**
551
+ * Start audio processor
552
+ */
553
+ async start() {
554
+ if (this.workletNode) return;
555
+ try {
556
+ const blob = new Blob([
557
+ AUDIO_WORKLET_CODE
558
+ ], {
559
+ type: "application/javascript"
560
+ });
561
+ const url = URL.createObjectURL(blob);
562
+ await this.context.audioWorklet.addModule(url);
563
+ this.workletNode = new AudioWorkletNode(this.context, "l8b-audio-processor");
564
+ this.workletNode.connect(this.context.destination);
565
+ this.flushBuffer();
566
+ } catch (e) {
567
+ (0, import_diagnostics.reportRuntimeError)(this.runtime?.listener, import_diagnostics.APIErrorCode.E7012, {
568
+ error: String(e)
569
+ });
570
+ }
571
+ }
572
+ /**
573
+ * Flush buffered messages
574
+ */
575
+ flushBuffer() {
576
+ if (!this.workletNode) return;
577
+ while (this.buffer.length > 0) {
578
+ this.workletNode.port.postMessage(this.buffer.splice(0, 1)[0]);
579
+ }
580
+ }
581
+ /**
582
+ * Get or create beeper
583
+ */
584
+ getBeeper() {
585
+ if (!this.beeper) {
586
+ this.beeper = new Beeper(this);
587
+ if (this.context.state === "running") {
588
+ this.start();
589
+ }
590
+ }
591
+ return this.beeper;
592
+ }
593
+ /**
594
+ * Play beep sequence
595
+ */
596
+ beep(sequence) {
597
+ this.getBeeper().beep(sequence);
598
+ }
599
+ /**
600
+ * Add beeps to audio processor
601
+ */
602
+ addBeeps(beeps) {
603
+ for (const b of beeps) {
604
+ b.duration *= this.context.sampleRate;
605
+ b.increment = b.frequency / this.context.sampleRate;
606
+ }
607
+ if (this.workletNode) {
608
+ this.workletNode.port.postMessage(JSON.stringify({
609
+ name: "beep",
610
+ sequence: beeps
611
+ }));
612
+ } else {
613
+ this.buffer.push(JSON.stringify({
614
+ name: "beep",
615
+ sequence: beeps
616
+ }));
617
+ }
618
+ }
619
+ /**
620
+ * Cancel all beeps
621
+ */
622
+ cancelBeeps() {
623
+ if (this.workletNode) {
624
+ this.workletNode.port.postMessage(JSON.stringify({
625
+ name: "cancel_beeps"
626
+ }));
627
+ } else {
628
+ this.buffer.push(JSON.stringify({
629
+ name: "cancel_beeps"
630
+ }));
631
+ }
632
+ this.stopAll();
633
+ }
634
+ /**
635
+ * Add playing sound/music to list
636
+ */
637
+ addPlaying(item) {
638
+ this.playing.push(item);
639
+ }
640
+ /**
641
+ * Remove playing sound/music from list
642
+ */
643
+ removePlaying(item) {
644
+ const index = this.playing.indexOf(item);
645
+ if (index >= 0) {
646
+ this.playing.splice(index, 1);
647
+ }
648
+ }
649
+ /**
650
+ * Stop all playing sounds/music
651
+ */
652
+ stopAll() {
653
+ for (const p of this.playing) {
654
+ try {
655
+ p.stop();
656
+ } catch (err) {
657
+ (0, import_diagnostics.reportRuntimeError)(this.runtime?.listener, import_diagnostics.APIErrorCode.E7016, {
658
+ error: String(err)
659
+ });
660
+ }
661
+ }
662
+ this.playing = [];
663
+ }
664
+ };
665
+
666
+ // src/devices/music.ts
667
+ var Music = class {
668
+ static {
669
+ __name(this, "Music");
670
+ }
671
+ ready = 1;
672
+ name = "";
673
+ url;
674
+ tag;
675
+ playing = false;
676
+ audio;
677
+ constructor(audio, url) {
678
+ this.audio = audio;
679
+ this.url = url;
680
+ this.tag = new Audio(this.url);
681
+ this.ready = 1;
682
+ }
683
+ /**
684
+ * Play music with volume and loop (HTML5 Audio implementation)
685
+ */
686
+ play(volume = 1, loopit = false) {
687
+ this.playing = true;
688
+ this.tag.loop = !!loopit;
689
+ this.tag.volume = Math.max(0, Math.min(1, volume));
690
+ if (this.audio.isStarted()) {
691
+ this.tag.play();
692
+ } else {
693
+ this.audio.addToWakeUpList(this);
694
+ }
695
+ this.audio.addPlaying(this);
696
+ return {
697
+ play: /* @__PURE__ */ __name(() => {
698
+ return this.tag.play();
699
+ }, "play"),
700
+ stop: /* @__PURE__ */ __name(() => {
701
+ this.playing = false;
702
+ this.tag.pause();
703
+ this.audio.removePlaying(this);
704
+ }, "stop"),
705
+ setVolume: /* @__PURE__ */ __name((v) => {
706
+ this.tag.volume = Math.max(0, Math.min(1, v));
707
+ }, "setVolume"),
708
+ getPosition: /* @__PURE__ */ __name(() => {
709
+ return this.tag.currentTime;
710
+ }, "getPosition"),
711
+ getDuration: /* @__PURE__ */ __name(() => {
712
+ return this.tag.duration;
713
+ }, "getDuration"),
714
+ setPosition: /* @__PURE__ */ __name((pos) => {
715
+ this.tag.pause();
716
+ this.tag.currentTime = Math.max(0, Math.min(this.tag.duration, pos));
717
+ if (this.playing) {
718
+ this.tag.play();
719
+ }
720
+ }, "setPosition")
721
+ };
722
+ }
723
+ /**
724
+ * Wake up audio (for autoplay policy)
725
+ */
726
+ wakeUp() {
727
+ if (this.playing) {
728
+ this.tag.play();
729
+ }
730
+ }
731
+ /**
732
+ * Stop music
733
+ */
734
+ stop() {
735
+ this.playing = false;
736
+ this.tag.pause();
737
+ this.audio.removePlaying(this);
738
+ }
739
+ };
740
+
741
+ // src/devices/sound.ts
742
+ var import_diagnostics2 = require("@al8b/diagnostics");
743
+ var Sound = class _Sound {
744
+ static {
745
+ __name(this, "Sound");
746
+ }
747
+ ready = 0;
748
+ buffer;
749
+ name = "";
750
+ url;
751
+ audio;
752
+ constructor(audio, url) {
753
+ this.audio = audio;
754
+ if (url instanceof AudioBuffer) {
755
+ this.buffer = url;
756
+ this.url = "";
757
+ this.ready = 1;
758
+ } else {
759
+ this.url = url;
760
+ this.loadSound(url);
761
+ }
762
+ }
763
+ /**
764
+ * Load sound from URL
765
+ */
766
+ loadSound(url) {
767
+ const request = new XMLHttpRequest();
768
+ request.open("GET", url, true);
769
+ request.responseType = "arraybuffer";
770
+ request.onload = () => {
771
+ this.audio.context.decodeAudioData(request.response, (buffer) => {
772
+ this.buffer = buffer;
773
+ this.ready = 1;
774
+ }, (err) => {
775
+ (0, import_diagnostics2.reportRuntimeError)(this.audio?.runtime?.listener, import_diagnostics2.APIErrorCode.E7016, {
776
+ error: `Audio decoding failed: ${String(err)}`
777
+ });
778
+ });
779
+ };
780
+ request.onerror = () => {
781
+ (0, import_diagnostics2.reportRuntimeError)(this.audio?.runtime?.listener, import_diagnostics2.APIErrorCode.E7016, {
782
+ error: `Failed to load sound: ${url}`
783
+ });
784
+ };
785
+ request.send();
786
+ }
787
+ /**
788
+ * Play sound with volume, pitch, pan, and loop
789
+ */
790
+ play(volume = 1, pitch = 1, pan = 0, loopit = false) {
791
+ if (!this.buffer) return {
792
+ stop: /* @__PURE__ */ __name(() => {
793
+ }, "stop"),
794
+ finished: true
795
+ };
796
+ const context = this.audio.context;
797
+ const source = context.createBufferSource();
798
+ source.playbackRate.value = pitch;
799
+ source.buffer = this.buffer;
800
+ if (loopit) source.loop = true;
801
+ const gain = context.createGain();
802
+ gain.gain.value = volume;
803
+ const panner = context.createPanner();
804
+ panner.panningModel = "equalpower";
805
+ panner.setPan = (p) => {
806
+ panner.setPosition(p, 0, 1 - Math.abs(p));
807
+ };
808
+ panner.setPan(pan);
809
+ source.connect(gain);
810
+ gain.connect(panner);
811
+ panner.connect(context.destination);
812
+ source.start();
813
+ let playing = null;
814
+ if (loopit) {
815
+ playing = {
816
+ stop: /* @__PURE__ */ __name(() => {
817
+ source.stop();
818
+ }, "stop")
819
+ };
820
+ this.audio.addPlaying(playing);
821
+ }
822
+ const res = {
823
+ stop: /* @__PURE__ */ __name(() => {
824
+ source.stop();
825
+ if (playing) this.audio.removePlaying(playing);
826
+ return 1;
827
+ }, "stop"),
828
+ setVolume: /* @__PURE__ */ __name((v) => {
829
+ gain.gain.value = Math.max(0, Math.min(1, v));
830
+ }, "setVolume"),
831
+ setPitch: /* @__PURE__ */ __name((p) => {
832
+ source.playbackRate.value = Math.max(1e-3, Math.min(1e3, p));
833
+ }, "setPitch"),
834
+ setPan: /* @__PURE__ */ __name((p) => {
835
+ panner.setPan(Math.max(-1, Math.min(1, p)));
836
+ }, "setPan"),
837
+ getDuration: /* @__PURE__ */ __name(() => {
838
+ return source.buffer ? source.buffer.duration : 0;
839
+ }, "getDuration"),
840
+ finished: false
841
+ };
842
+ source.onended = () => {
843
+ res.finished = true;
844
+ };
845
+ return res;
846
+ }
847
+ /**
848
+ * Create MicroSound class for procedural sound generation
849
+ */
850
+ static createSoundClass(audiocore) {
851
+ return class MicroSound {
852
+ static {
853
+ __name(this, "MicroSound");
854
+ }
855
+ static classname = "Sound";
856
+ channels;
857
+ length;
858
+ sampleRate;
859
+ sound;
860
+ buffer;
861
+ constructor(channels, length, sampleRate = 44100) {
862
+ channels = channels === 1 ? 1 : 2;
863
+ if (!(length > 1 && length < 44100 * 1e3)) {
864
+ length = 44100;
865
+ }
866
+ if (!(sampleRate >= 8e3 && sampleRate <= 96e3)) {
867
+ sampleRate = 44100;
868
+ }
869
+ this.channels = channels;
870
+ this.length = length;
871
+ this.sampleRate = sampleRate;
872
+ this.buffer = audiocore.context.createBuffer(channels, length, sampleRate);
873
+ this.sound = new _Sound(audiocore, this.buffer);
874
+ }
875
+ /**
876
+ * Play the sound
877
+ */
878
+ play(volume, pitch, pan, loopit) {
879
+ return this.sound.play(volume, pitch, pan, loopit);
880
+ }
881
+ /**
882
+ * Write sample value to buffer
883
+ */
884
+ write(channel, position, value) {
885
+ if (channel === 0) {
886
+ const ch1 = this.buffer.getChannelData(0);
887
+ ch1[position] = value;
888
+ } else if (this.channels === 2) {
889
+ const ch2 = this.buffer.getChannelData(1);
890
+ ch2[position] = value;
891
+ }
892
+ }
893
+ /**
894
+ * Read sample value from buffer
895
+ */
896
+ read(channel, position) {
897
+ if (channel === 0) {
898
+ const ch1 = this.buffer.getChannelData(0);
899
+ return ch1[position];
900
+ } else if (this.channels === 2) {
901
+ const ch2 = this.buffer.getChannelData(1);
902
+ return ch2[position];
903
+ }
904
+ return 0;
905
+ }
906
+ };
907
+ }
908
+ };
909
+ // Annotate the CommonJS export names for ESM import in node:
910
+ 0 && (module.exports = {
911
+ AudioCore,
912
+ Beeper,
913
+ Music,
914
+ Sound
915
+ });
916
+ //# sourceMappingURL=index.js.map