@students-dev/audify-js 1.0.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.
@@ -0,0 +1,1477 @@
1
+ import { promises } from 'fs';
2
+ import { extname } from 'path';
3
+
4
+ /**
5
+ * Loop modes for playback
6
+ */
7
+ const LOOP_MODES$1 = {
8
+ OFF: 'off',
9
+ TRACK: 'track',
10
+ QUEUE: 'queue'
11
+ };
12
+
13
+ /**
14
+ * Repeat modes (alias for loop modes)
15
+ */
16
+ const REPEAT_MODES = LOOP_MODES$1;
17
+
18
+ /**
19
+ * Filter types
20
+ */
21
+ const FILTER_TYPES = {
22
+ BASSBOOST: 'bassboost',
23
+ TREBLEBOOST: 'trebleboost',
24
+ NIGHTCORE: 'nightcore',
25
+ VAPORWAVE: 'vaporwave',
26
+ ROTATE_8D: '8d',
27
+ PITCH: 'pitch',
28
+ SPEED: 'speed',
29
+ REVERB: 'reverb'
30
+ };
31
+
32
+ /**
33
+ * Event types
34
+ */
35
+ const EVENTS = {
36
+ READY: 'ready',
37
+ PLAY: 'play',
38
+ PAUSE: 'pause',
39
+ STOP: 'stop',
40
+ ERROR: 'error',
41
+ QUEUE_EMPTY: 'queueEmpty',
42
+ TRACK_START: 'trackStart',
43
+ TRACK_END: 'trackEnd',
44
+ FILTER_APPLIED: 'filterApplied',
45
+ TRACK_ADD: 'trackAdd',
46
+ TRACK_REMOVE: 'trackRemove',
47
+ SHUFFLE: 'shuffle',
48
+ CLEAR: 'clear'
49
+ };
50
+
51
+ /**
52
+ * Simple event emitter for handling events
53
+ */
54
+ class EventBus {
55
+ constructor() {
56
+ this.events = {};
57
+ }
58
+
59
+ /**
60
+ * Register an event listener
61
+ * @param {string} event - Event name
62
+ * @param {Function} callback - Callback function
63
+ */
64
+ on(event, callback) {
65
+ if (!this.events[event]) {
66
+ this.events[event] = [];
67
+ }
68
+ this.events[event].push(callback);
69
+ }
70
+
71
+ /**
72
+ * Remove an event listener
73
+ * @param {string} event - Event name
74
+ * @param {Function} callback - Callback function
75
+ */
76
+ off(event, callback) {
77
+ if (!this.events[event]) return;
78
+ this.events[event] = this.events[event].filter(cb => cb !== callback);
79
+ }
80
+
81
+ /**
82
+ * Emit an event
83
+ * @param {string} event - Event name
84
+ * @param {*} data - Data to pass to listeners
85
+ */
86
+ emit(event, data) {
87
+ if (!this.events[event]) return;
88
+ this.events[event].forEach(callback => {
89
+ try {
90
+ callback(data);
91
+ } catch (error) {
92
+ console.error(`Error in event listener for ${event}:`, error);
93
+ }
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Remove all listeners for an event
99
+ * @param {string} event - Event name
100
+ */
101
+ removeAllListeners(event) {
102
+ delete this.events[event];
103
+ }
104
+
105
+ /**
106
+ * Get all listeners for an event
107
+ * @param {string} event - Event name
108
+ * @returns {Function[]} Array of listeners
109
+ */
110
+ listeners(event) {
111
+ return this.events[event] || [];
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Audio player with playback controls
117
+ */
118
+ class Player {
119
+ constructor(audioEngine) {
120
+ this.audioEngine = audioEngine;
121
+ this.audioContext = audioEngine.audioContext;
122
+ this.source = null;
123
+ this.isPlaying = false;
124
+ this.currentTime = 0;
125
+ this.duration = 0;
126
+ this.volume = 1;
127
+ this.loopMode = LOOP_MODES$1.OFF;
128
+ this.eventBus = new EventBus();
129
+ }
130
+
131
+ /**
132
+ * Play audio
133
+ * @param {Track} track - Track to play
134
+ */
135
+ async play(track) {
136
+ if (!track) return;
137
+
138
+ try {
139
+ await this.loadTrack(track);
140
+ this.source.start(0);
141
+ this.isPlaying = true;
142
+ this.eventBus.emit(EVENTS.PLAY, track);
143
+ this.eventBus.emit(EVENTS.TRACK_START, track);
144
+ } catch (error) {
145
+ this.eventBus.emit(EVENTS.ERROR, error);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Pause playback
151
+ */
152
+ pause() {
153
+ if (this.source && this.isPlaying) {
154
+ this.source.stop();
155
+ this.isPlaying = false;
156
+ this.eventBus.emit(EVENTS.PAUSE);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Resume playback
162
+ */
163
+ resume() {
164
+ if (!this.isPlaying && this.source) {
165
+ // Would need to recreate source for resume
166
+ this.eventBus.emit(EVENTS.PLAY);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Stop playback
172
+ */
173
+ stop() {
174
+ if (this.source) {
175
+ this.source.stop();
176
+ this.source = null;
177
+ this.isPlaying = false;
178
+ this.currentTime = 0;
179
+ this.eventBus.emit(EVENTS.STOP);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Seek to position
185
+ * @param {number} time - Time in seconds
186
+ */
187
+ seek(time) {
188
+ // In Web Audio API, seeking requires buffer manipulation
189
+ this.currentTime = Math.max(0, Math.min(time, this.duration));
190
+ }
191
+
192
+ /**
193
+ * Set volume
194
+ * @param {number} volume - Volume level (0-1)
195
+ */
196
+ setVolume(volume) {
197
+ this.volume = Math.max(0, Math.min(1, volume));
198
+ // Apply to gain node if exists
199
+ }
200
+
201
+ /**
202
+ * Set loop mode
203
+ * @param {string} mode - Loop mode
204
+ */
205
+ setLoopMode(mode) {
206
+ this.loopMode = mode;
207
+ }
208
+
209
+ /**
210
+ * Load track into player
211
+ * @param {Track} track - Track to load
212
+ */
213
+ async loadTrack(track) {
214
+ if (!this.audioContext) throw new Error('AudioContext not available');
215
+
216
+ const response = await fetch(track.url);
217
+ const arrayBuffer = await response.arrayBuffer();
218
+ const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
219
+
220
+ this.source = this.audioContext.createBufferSource();
221
+ this.source.buffer = audioBuffer;
222
+ this.duration = audioBuffer.duration;
223
+
224
+ // Connect through filters
225
+ this.audioEngine.filters.connect(this.source, this.audioContext.destination);
226
+
227
+ // Handle end of track
228
+ this.source.onended = () => {
229
+ this.isPlaying = false;
230
+ this.eventBus.emit(EVENTS.TRACK_END, track);
231
+ this.handleTrackEnd();
232
+ };
233
+ }
234
+
235
+ /**
236
+ * Handle track end based on loop mode
237
+ */
238
+ handleTrackEnd() {
239
+ switch (this.loopMode) {
240
+ case LOOP_MODES$1.TRACK:
241
+ // Replay current track
242
+ break;
243
+ case LOOP_MODES$1.QUEUE:
244
+ // Play next in queue
245
+ this.audioEngine.queue.getNext();
246
+ break;
247
+ case LOOP_MODES$1.OFF:
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Get current playback state
253
+ * @returns {Object} State object
254
+ */
255
+ getState() {
256
+ return {
257
+ isPlaying: this.isPlaying,
258
+ currentTime: this.currentTime,
259
+ duration: this.duration,
260
+ volume: this.volume,
261
+ loopMode: this.loopMode
262
+ };
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Audio filters and effects
268
+ */
269
+ class Filters {
270
+ constructor(audioContext) {
271
+ this.audioContext = audioContext;
272
+ this.filters = new Map();
273
+ this.enabled = new Set();
274
+ }
275
+
276
+ /**
277
+ * Apply filter
278
+ * @param {string} type - Filter type
279
+ * @param {Object} options - Filter options
280
+ */
281
+ apply(type, options = {}) {
282
+ if (!this.audioContext) return;
283
+
284
+ switch (type) {
285
+ case FILTER_TYPES.BASSBOOST:
286
+ this.applyBassBoost(options);
287
+ break;
288
+ case FILTER_TYPES.NIGHTCORE:
289
+ this.applyNightcore(options);
290
+ break;
291
+ case FILTER_TYPES.VAPORWAVE:
292
+ this.applyVaporwave(options);
293
+ break;
294
+ case FILTER_TYPES.ROTATE_8D:
295
+ this.apply8DRotate(options);
296
+ break;
297
+ case FILTER_TYPES.PITCH:
298
+ this.applyPitch(options);
299
+ break;
300
+ case FILTER_TYPES.SPEED:
301
+ this.applySpeed(options);
302
+ break;
303
+ case FILTER_TYPES.REVERB:
304
+ this.applyReverb(options);
305
+ break;
306
+ }
307
+
308
+ this.enabled.add(type);
309
+ }
310
+
311
+ /**
312
+ * Remove filter
313
+ * @param {string} type - Filter type
314
+ */
315
+ remove(type) {
316
+ if (this.filters.has(type)) {
317
+ const filter = this.filters.get(type);
318
+ filter.disconnect();
319
+ this.filters.delete(type);
320
+ this.enabled.delete(type);
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Clear all filters
326
+ */
327
+ clear() {
328
+ this.filters.forEach(filter => filter.disconnect());
329
+ this.filters.clear();
330
+ this.enabled.clear();
331
+ }
332
+
333
+ /**
334
+ * Check if filter is enabled
335
+ * @param {string} type - Filter type
336
+ * @returns {boolean} Is enabled
337
+ */
338
+ isEnabled(type) {
339
+ return this.enabled.has(type);
340
+ }
341
+
342
+ /**
343
+ * Get enabled filters
344
+ * @returns {Set} Enabled filter types
345
+ */
346
+ getEnabled() {
347
+ return new Set(this.enabled);
348
+ }
349
+
350
+ // Filter implementations
351
+ applyBassBoost(options = {}) {
352
+ const gain = options.gain || 1.5;
353
+ const filter = this.audioContext.createBiquadFilter();
354
+ filter.type = 'lowshelf';
355
+ filter.frequency.value = 200;
356
+ filter.gain.value = gain * 10;
357
+ this.filters.set(FILTER_TYPES.BASSBOOST, filter);
358
+ }
359
+
360
+ applyNightcore(options = {}) {
361
+ const rate = options.rate || 1.2;
362
+ // Nightcore is pitch + speed up
363
+ this.applyPitch({ pitch: rate });
364
+ this.applySpeed({ speed: rate });
365
+ }
366
+
367
+ applyVaporwave(options = {}) {
368
+ const rate = options.rate || 0.8;
369
+ this.applyPitch({ pitch: rate });
370
+ this.applySpeed({ speed: rate });
371
+ }
372
+
373
+ apply8DRotate(options = {}) {
374
+ // 8D audio effect using panner
375
+ const panner = this.audioContext.createPanner();
376
+ panner.panningModel = 'HRTF';
377
+ // Would need to animate the position for rotation
378
+ this.filters.set(FILTER_TYPES.ROTATE_8D, panner);
379
+ }
380
+
381
+ applyPitch(options = {}) {
382
+ options.pitch || 1;
383
+ // In Web Audio API, pitch shifting requires AudioWorklet or external library
384
+ // For simplicity, we'll use a basic implementation
385
+ console.warn('Pitch shifting requires AudioWorklet in modern browsers');
386
+ }
387
+
388
+ applySpeed(options = {}) {
389
+ const speed = options.speed || 1;
390
+ // Speed change affects playback rate
391
+ // This would be handled in the player
392
+ console.log(`Speed filter applied: ${speed}x`);
393
+ }
394
+
395
+ applyReverb(options = {}) {
396
+ const convolver = this.audioContext.createConvolver();
397
+ // Would need an impulse response for reverb
398
+ // For simplicity, create a basic reverb
399
+ this.filters.set(FILTER_TYPES.REVERB, convolver);
400
+ }
401
+
402
+ /**
403
+ * Connect filters to audio node
404
+ * @param {AudioNode} input - Input node
405
+ * @param {AudioNode} output - Output node
406
+ */
407
+ connect(input, output) {
408
+ let currentNode = input;
409
+
410
+ this.filters.forEach(filter => {
411
+ currentNode.connect(filter);
412
+ currentNode = filter;
413
+ });
414
+
415
+ currentNode.connect(output);
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Metadata parsing utilities
421
+ */
422
+ class MetadataUtils {
423
+ /**
424
+ * Extract basic metadata from URL or file path
425
+ * @param {string} source - URL or file path
426
+ * @returns {Object} Metadata object
427
+ */
428
+ static extract(source) {
429
+ if (!source) return {};
430
+
431
+ const metadata = {
432
+ title: this.extractTitle(source),
433
+ artist: null,
434
+ duration: null,
435
+ thumbnail: null
436
+ };
437
+
438
+ // For YouTube URLs
439
+ if (source.includes('youtube.com') || source.includes('youtu.be')) {
440
+ return this.extractYouTubeMetadata(source);
441
+ }
442
+
443
+ // For SoundCloud URLs
444
+ if (source.includes('soundcloud.com')) {
445
+ return this.extractSoundCloudMetadata(source);
446
+ }
447
+
448
+ // For local files
449
+ if (!source.startsWith('http')) {
450
+ return this.extractFileMetadata(source);
451
+ }
452
+
453
+ return metadata;
454
+ }
455
+
456
+ /**
457
+ * Extract title from source
458
+ * @param {string} source - Source string
459
+ * @returns {string} Extracted title
460
+ */
461
+ static extractTitle(source) {
462
+ if (!source) return 'Unknown Track';
463
+
464
+ // Try to extract from URL query params
465
+ try {
466
+ const url = new URL(source);
467
+ const title = url.searchParams.get('title') || url.searchParams.get('name');
468
+ if (title) return decodeURIComponent(title);
469
+ } catch {}
470
+
471
+ // Extract from file path
472
+ const parts = source.split('/').pop().split('\\').pop();
473
+ if (parts) {
474
+ return parts.replace(/\.[^/.]+$/, ''); // Remove extension
475
+ }
476
+
477
+ return 'Unknown Track';
478
+ }
479
+
480
+ /**
481
+ * Extract YouTube metadata (basic)
482
+ * @param {string} url - YouTube URL
483
+ * @returns {Object} Metadata
484
+ */
485
+ static extractYouTubeMetadata(url) {
486
+ // Basic extraction, in real implementation would fetch from API
487
+ return {
488
+ title: 'YouTube Track',
489
+ artist: null,
490
+ duration: null,
491
+ thumbnail: null,
492
+ source: 'youtube'
493
+ };
494
+ }
495
+
496
+ /**
497
+ * Extract SoundCloud metadata (basic)
498
+ * @param {string} url - SoundCloud URL
499
+ * @returns {Object} Metadata
500
+ */
501
+ static extractSoundCloudMetadata(url) {
502
+ // Basic extraction
503
+ return {
504
+ title: 'SoundCloud Track',
505
+ artist: null,
506
+ duration: null,
507
+ thumbnail: null,
508
+ source: 'soundcloud'
509
+ };
510
+ }
511
+
512
+ /**
513
+ * Extract file metadata (basic)
514
+ * @param {string} path - File path
515
+ * @returns {Object} Metadata
516
+ */
517
+ static extractFileMetadata(path) {
518
+ const title = this.extractTitle(path);
519
+ return {
520
+ title,
521
+ artist: null,
522
+ duration: null,
523
+ thumbnail: null,
524
+ source: 'local'
525
+ };
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Represents an audio track
531
+ */
532
+ class Track {
533
+ /**
534
+ * @param {string} url - Track URL or file path
535
+ * @param {Object} options - Additional options
536
+ */
537
+ constructor(url, options = {}) {
538
+ this.url = url;
539
+ this.title = options.title || MetadataUtils.extract(url).title;
540
+ this.artist = options.artist || null;
541
+ this.duration = options.duration || null;
542
+ this.thumbnail = options.thumbnail || null;
543
+ this.metadata = options.metadata || {};
544
+ this.id = options.id || Math.random().toString(36).substr(2, 9);
545
+ }
546
+
547
+ /**
548
+ * Get track info
549
+ * @returns {Object} Track information
550
+ */
551
+ getInfo() {
552
+ return {
553
+ id: this.id,
554
+ url: this.url,
555
+ title: this.title,
556
+ artist: this.artist,
557
+ duration: this.duration,
558
+ thumbnail: this.thumbnail,
559
+ metadata: this.metadata
560
+ };
561
+ }
562
+
563
+ /**
564
+ * Update track metadata
565
+ * @param {Object} metadata - New metadata
566
+ */
567
+ updateMetadata(metadata) {
568
+ Object.assign(this.metadata, metadata);
569
+ }
570
+
571
+ /**
572
+ * Check if track is valid
573
+ * @returns {boolean} Is valid
574
+ */
575
+ isValid() {
576
+ return !!(this.url && this.title);
577
+ }
578
+ }
579
+
580
+ /**
581
+ * Audio queue management
582
+ */
583
+ class Queue {
584
+ constructor() {
585
+ this.tracks = [];
586
+ this.currentIndex = -1;
587
+ this.eventBus = new EventBus();
588
+ }
589
+
590
+ /**
591
+ * Add track(s) to queue
592
+ * @param {Track|Track[]|string|string[]} tracks - Track(s) to add
593
+ * @param {number} position - Position to insert (optional)
594
+ */
595
+ add(tracks, position) {
596
+ const trackArray = Array.isArray(tracks) ? tracks : [tracks];
597
+
598
+ const processedTracks = trackArray.map(track => {
599
+ if (typeof track === 'string') {
600
+ return new Track(track);
601
+ }
602
+ return track instanceof Track ? track : new Track(track.url, track);
603
+ });
604
+
605
+ if (position !== undefined && position >= 0 && position <= this.tracks.length) {
606
+ this.tracks.splice(position, 0, ...processedTracks);
607
+ } else {
608
+ this.tracks.push(...processedTracks);
609
+ }
610
+
611
+ processedTracks.forEach(track => {
612
+ this.eventBus.emit(EVENTS.TRACK_ADD, track);
613
+ });
614
+ }
615
+
616
+ /**
617
+ * Remove track from queue
618
+ * @param {number|string} identifier - Track index or ID
619
+ * @returns {Track|null} Removed track
620
+ */
621
+ remove(identifier) {
622
+ let index;
623
+ if (typeof identifier === 'number') {
624
+ index = identifier;
625
+ } else {
626
+ index = this.tracks.findIndex(track => track.id === identifier);
627
+ }
628
+
629
+ if (index < 0 || index >= this.tracks.length) return null;
630
+
631
+ const removed = this.tracks.splice(index, 1)[0];
632
+
633
+ if (this.currentIndex > index) {
634
+ this.currentIndex--;
635
+ } else if (this.currentIndex === index) {
636
+ this.currentIndex = -1;
637
+ }
638
+
639
+ this.eventBus.emit(EVENTS.TRACK_REMOVE, removed);
640
+ return removed;
641
+ }
642
+
643
+ /**
644
+ * Shuffle the queue
645
+ */
646
+ shuffle() {
647
+ if (this.tracks.length <= 1) return;
648
+
649
+ // Fisher-Yates shuffle
650
+ for (let i = this.tracks.length - 1; i > 0; i--) {
651
+ const j = Math.floor(Math.random() * (i + 1));
652
+ [this.tracks[i], this.tracks[j]] = [this.tracks[j], this.tracks[i]];
653
+ }
654
+
655
+ this.currentIndex = -1;
656
+ this.eventBus.emit(EVENTS.SHUFFLE, this.tracks);
657
+ }
658
+
659
+ /**
660
+ * Clear the queue
661
+ */
662
+ clear() {
663
+ this.tracks = [];
664
+ this.currentIndex = -1;
665
+ this.eventBus.emit(EVENTS.CLEAR);
666
+ }
667
+
668
+ /**
669
+ * Jump to specific track
670
+ * @param {number} index - Track index
671
+ * @returns {Track|null} Track at index
672
+ */
673
+ jump(index) {
674
+ if (index < 0 || index >= this.tracks.length) return null;
675
+ this.currentIndex = index;
676
+ return this.tracks[index];
677
+ }
678
+
679
+ /**
680
+ * Get current track
681
+ * @returns {Track|null} Current track
682
+ */
683
+ getCurrent() {
684
+ return this.currentIndex >= 0 ? this.tracks[this.currentIndex] : null;
685
+ }
686
+
687
+ /**
688
+ * Get next track
689
+ * @returns {Track|null} Next track
690
+ */
691
+ getNext() {
692
+ if (this.tracks.length === 0) return null;
693
+ this.currentIndex = (this.currentIndex + 1) % this.tracks.length;
694
+ return this.tracks[this.currentIndex];
695
+ }
696
+
697
+ /**
698
+ * Get previous track
699
+ * @returns {Track|null} Previous track
700
+ */
701
+ getPrevious() {
702
+ if (this.tracks.length === 0) return null;
703
+ this.currentIndex = this.currentIndex <= 0 ? this.tracks.length - 1 : this.currentIndex - 1;
704
+ return this.tracks[this.currentIndex];
705
+ }
706
+
707
+ /**
708
+ * Get all tracks
709
+ * @returns {Track[]} Array of tracks
710
+ */
711
+ getTracks() {
712
+ return [...this.tracks];
713
+ }
714
+
715
+ /**
716
+ * Get queue size
717
+ * @returns {number} Number of tracks
718
+ */
719
+ size() {
720
+ return this.tracks.length;
721
+ }
722
+
723
+ /**
724
+ * Check if queue is empty
725
+ * @returns {boolean} Is empty
726
+ */
727
+ isEmpty() {
728
+ return this.tracks.length === 0;
729
+ }
730
+
731
+ /**
732
+ * Get track at index
733
+ * @param {number} index - Track index
734
+ * @returns {Track|null} Track at index
735
+ */
736
+ getTrack(index) {
737
+ return index >= 0 && index < this.tracks.length ? this.tracks[index] : null;
738
+ }
739
+ }
740
+
741
+ /**
742
+ * Main audio engine class
743
+ */
744
+ class AudioEngine {
745
+ constructor(options = {}) {
746
+ this.options = options;
747
+ this.audioContext = null;
748
+ this.player = null;
749
+ this.filters = null;
750
+ this.queue = new Queue();
751
+ this.eventBus = new EventBus();
752
+ this.isReady = false;
753
+
754
+ this.initialize();
755
+ }
756
+
757
+ /**
758
+ * Initialize the audio engine
759
+ */
760
+ async initialize() {
761
+ try {
762
+ // Create AudioContext
763
+ this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
764
+
765
+ // Create components
766
+ this.filters = new Filters(this.audioContext);
767
+ this.player = new Player(this);
768
+
769
+ // Connect event buses
770
+ this.queue.eventBus.on(EVENTS.TRACK_ADD, (track) => this.eventBus.emit(EVENTS.TRACK_ADD, track));
771
+ this.queue.eventBus.on(EVENTS.TRACK_REMOVE, (track) => this.eventBus.emit(EVENTS.TRACK_REMOVE, track));
772
+ this.player.eventBus.on(EVENTS.PLAY, (data) => this.eventBus.emit(EVENTS.PLAY, data));
773
+ this.player.eventBus.on(EVENTS.PAUSE, () => this.eventBus.emit(EVENTS.PAUSE));
774
+ this.player.eventBus.on(EVENTS.STOP, () => this.eventBus.emit(EVENTS.STOP));
775
+ this.player.eventBus.on(EVENTS.ERROR, (error) => this.eventBus.emit(EVENTS.ERROR, error));
776
+ this.player.eventBus.on(EVENTS.TRACK_START, (track) => this.eventBus.emit(EVENTS.TRACK_START, track));
777
+ this.player.eventBus.on(EVENTS.TRACK_END, (track) => this.eventBus.emit(EVENTS.TRACK_END, track));
778
+
779
+ this.isReady = true;
780
+ this.eventBus.emit(EVENTS.READY);
781
+ } catch (error) {
782
+ this.eventBus.emit(EVENTS.ERROR, error);
783
+ }
784
+ }
785
+
786
+ /**
787
+ * Play track or resume playback
788
+ * @param {Track|string} track - Track to play or track identifier
789
+ */
790
+ async play(track) {
791
+ if (!this.isReady) return;
792
+
793
+ if (track) {
794
+ const trackObj = typeof track === 'string' ? this.queue.getCurrent() : track;
795
+ await this.player.play(trackObj);
796
+ } else {
797
+ this.player.resume();
798
+ }
799
+ }
800
+
801
+ /**
802
+ * Pause playback
803
+ */
804
+ pause() {
805
+ this.player.pause();
806
+ }
807
+
808
+ /**
809
+ * Stop playback
810
+ */
811
+ stop() {
812
+ this.player.stop();
813
+ }
814
+
815
+ /**
816
+ * Seek to position
817
+ * @param {number} time - Time in seconds
818
+ */
819
+ seek(time) {
820
+ this.player.seek(time);
821
+ }
822
+
823
+ /**
824
+ * Set volume
825
+ * @param {number} volume - Volume level (0-1)
826
+ */
827
+ setVolume(volume) {
828
+ this.player.setVolume(volume);
829
+ }
830
+
831
+ /**
832
+ * Add track(s) to queue
833
+ * @param {Track|Track[]|string|string[]} tracks - Track(s) to add
834
+ */
835
+ add(tracks) {
836
+ this.queue.add(tracks);
837
+ }
838
+
839
+ /**
840
+ * Remove track from queue
841
+ * @param {number|string} identifier - Track index or ID
842
+ */
843
+ remove(identifier) {
844
+ return this.queue.remove(identifier);
845
+ }
846
+
847
+ /**
848
+ * Skip to next track
849
+ */
850
+ next() {
851
+ const nextTrack = this.queue.getNext();
852
+ if (nextTrack) {
853
+ this.play(nextTrack);
854
+ }
855
+ }
856
+
857
+ /**
858
+ * Go to previous track
859
+ */
860
+ previous() {
861
+ const prevTrack = this.queue.getPrevious();
862
+ if (prevTrack) {
863
+ this.play(prevTrack);
864
+ }
865
+ }
866
+
867
+ /**
868
+ * Shuffle queue
869
+ */
870
+ shuffle() {
871
+ this.queue.shuffle();
872
+ }
873
+
874
+ /**
875
+ * Clear queue
876
+ */
877
+ clear() {
878
+ this.queue.clear();
879
+ }
880
+
881
+ /**
882
+ * Jump to track in queue
883
+ * @param {number} index - Track index
884
+ */
885
+ jump(index) {
886
+ const track = this.queue.jump(index);
887
+ if (track) {
888
+ this.play(track);
889
+ }
890
+ }
891
+
892
+ /**
893
+ * Apply audio filter
894
+ * @param {string} type - Filter type
895
+ * @param {Object} options - Filter options
896
+ */
897
+ applyFilter(type, options) {
898
+ this.filters.apply(type, options);
899
+ this.eventBus.emit(EVENTS.FILTER_APPLIED, { type, options });
900
+ }
901
+
902
+ /**
903
+ * Remove audio filter
904
+ * @param {string} type - Filter type
905
+ */
906
+ removeFilter(type) {
907
+ this.filters.remove(type);
908
+ }
909
+
910
+ /**
911
+ * Set loop mode
912
+ * @param {string} mode - Loop mode
913
+ */
914
+ setLoopMode(mode) {
915
+ this.player.setLoopMode(mode);
916
+ }
917
+
918
+ /**
919
+ * Get current state
920
+ * @returns {Object} Engine state
921
+ */
922
+ getState() {
923
+ return {
924
+ isReady: this.isReady,
925
+ isPlaying: this.player ? this.player.isPlaying : false,
926
+ currentTrack: this.queue.getCurrent(),
927
+ queue: this.queue.getTracks(),
928
+ volume: this.player ? this.player.volume : 1,
929
+ loopMode: this.player ? this.player.loopMode : LOOP_MODES.OFF,
930
+ filters: this.filters ? this.filters.getEnabled() : new Set()
931
+ };
932
+ }
933
+
934
+ /**
935
+ * Destroy the engine
936
+ */
937
+ destroy() {
938
+ if (this.audioContext) {
939
+ this.audioContext.close();
940
+ }
941
+ this.filters.clear();
942
+ this.player.stop();
943
+ }
944
+ }
945
+
946
+ /**
947
+ * Base plugin class
948
+ */
949
+ class Plugin {
950
+ constructor(name, version = '1.0.0') {
951
+ this.name = name;
952
+ this.version = version;
953
+ this.enabled = false;
954
+ this.loaded = false;
955
+ }
956
+
957
+ /**
958
+ * Called when plugin is loaded
959
+ * @param {AudioEngine} engine - Audio engine instance
960
+ */
961
+ onLoad(engine) {
962
+ this.engine = engine;
963
+ this.loaded = true;
964
+ }
965
+
966
+ /**
967
+ * Called when plugin is enabled
968
+ */
969
+ onEnable() {
970
+ this.enabled = true;
971
+ }
972
+
973
+ /**
974
+ * Called when plugin is disabled
975
+ */
976
+ onDisable() {
977
+ this.enabled = false;
978
+ }
979
+
980
+ /**
981
+ * Hook called before play
982
+ * @param {Track} track - Track being played
983
+ */
984
+ beforePlay(track) {
985
+ // Override in subclass
986
+ }
987
+
988
+ /**
989
+ * Hook called after play
990
+ * @param {Track} track - Track being played
991
+ */
992
+ afterPlay(track) {
993
+ // Override in subclass
994
+ }
995
+
996
+ /**
997
+ * Hook called when track ends
998
+ * @param {Track} track - Track that ended
999
+ */
1000
+ trackEnd(track) {
1001
+ // Override in subclass
1002
+ }
1003
+
1004
+ /**
1005
+ * Hook called when queue updates
1006
+ * @param {Queue} queue - Updated queue
1007
+ */
1008
+ queueUpdate(queue) {
1009
+ // Override in subclass
1010
+ }
1011
+
1012
+ /**
1013
+ * Get plugin info
1014
+ * @returns {Object} Plugin information
1015
+ */
1016
+ getInfo() {
1017
+ return {
1018
+ name: this.name,
1019
+ version: this.version,
1020
+ enabled: this.enabled,
1021
+ loaded: this.loaded
1022
+ };
1023
+ }
1024
+ }
1025
+
1026
+ /**
1027
+ * Plugin manager for loading and managing plugins
1028
+ */
1029
+ class PluginManager {
1030
+ constructor(audioEngine) {
1031
+ this.engine = audioEngine;
1032
+ this.plugins = new Map();
1033
+ }
1034
+
1035
+ /**
1036
+ * Load a plugin
1037
+ * @param {Plugin} plugin - Plugin instance
1038
+ */
1039
+ load(plugin) {
1040
+ if (!(plugin instanceof Plugin)) {
1041
+ throw new Error('Invalid plugin instance');
1042
+ }
1043
+
1044
+ plugin.onLoad(this.engine);
1045
+ this.plugins.set(plugin.name, plugin);
1046
+ }
1047
+
1048
+ /**
1049
+ * Enable a plugin
1050
+ * @param {string} name - Plugin name
1051
+ */
1052
+ enable(name) {
1053
+ const plugin = this.plugins.get(name);
1054
+ if (plugin && !plugin.enabled) {
1055
+ plugin.onEnable();
1056
+ }
1057
+ }
1058
+
1059
+ /**
1060
+ * Disable a plugin
1061
+ * @param {string} name - Plugin name
1062
+ */
1063
+ disable(name) {
1064
+ const plugin = this.plugins.get(name);
1065
+ if (plugin && plugin.enabled) {
1066
+ plugin.onDisable();
1067
+ }
1068
+ }
1069
+
1070
+ /**
1071
+ * Unload a plugin
1072
+ * @param {string} name - Plugin name
1073
+ */
1074
+ unload(name) {
1075
+ const plugin = this.plugins.get(name);
1076
+ if (plugin) {
1077
+ if (plugin.enabled) {
1078
+ plugin.onDisable();
1079
+ }
1080
+ this.plugins.delete(name);
1081
+ }
1082
+ }
1083
+
1084
+ /**
1085
+ * Get plugin by name
1086
+ * @param {string} name - Plugin name
1087
+ * @returns {Plugin|null} Plugin instance
1088
+ */
1089
+ get(name) {
1090
+ return this.plugins.get(name) || null;
1091
+ }
1092
+
1093
+ /**
1094
+ * Get all plugins
1095
+ * @returns {Map} Map of plugins
1096
+ */
1097
+ getAll() {
1098
+ return new Map(this.plugins);
1099
+ }
1100
+
1101
+ /**
1102
+ * Get enabled plugins
1103
+ * @returns {Plugin[]} Array of enabled plugins
1104
+ */
1105
+ getEnabled() {
1106
+ return Array.from(this.plugins.values()).filter(plugin => plugin.enabled);
1107
+ }
1108
+
1109
+ /**
1110
+ * Call hook on all enabled plugins
1111
+ * @param {string} hook - Hook name
1112
+ * @param {...*} args - Arguments to pass
1113
+ */
1114
+ callHook(hook, ...args) {
1115
+ this.getEnabled().forEach(plugin => {
1116
+ if (typeof plugin[hook] === 'function') {
1117
+ try {
1118
+ plugin[hook](...args);
1119
+ } catch (error) {
1120
+ console.error(`Error in plugin ${plugin.name} hook ${hook}:`, error);
1121
+ }
1122
+ }
1123
+ });
1124
+ }
1125
+ }
1126
+
1127
+ /**
1128
+ * Logger utility with different levels
1129
+ */
1130
+ class Logger {
1131
+ constructor(level = 'info') {
1132
+ this.levels = {
1133
+ debug: 0,
1134
+ info: 1,
1135
+ warn: 2,
1136
+ error: 3
1137
+ };
1138
+ this.currentLevel = this.levels[level] || this.levels.info;
1139
+ }
1140
+
1141
+ /**
1142
+ * Set log level
1143
+ * @param {string} level - Log level (debug, info, warn, error)
1144
+ */
1145
+ setLevel(level) {
1146
+ this.currentLevel = this.levels[level] || this.levels.info;
1147
+ }
1148
+
1149
+ /**
1150
+ * Debug log
1151
+ * @param {...*} args - Arguments to log
1152
+ */
1153
+ debug(...args) {
1154
+ if (this.currentLevel <= this.levels.debug) {
1155
+ console.debug('[DEBUG]', ...args);
1156
+ }
1157
+ }
1158
+
1159
+ /**
1160
+ * Info log
1161
+ * @param {...*} args - Arguments to log
1162
+ */
1163
+ info(...args) {
1164
+ if (this.currentLevel <= this.levels.info) {
1165
+ console.info('[INFO]', ...args);
1166
+ }
1167
+ }
1168
+
1169
+ /**
1170
+ * Warning log
1171
+ * @param {...*} args - Arguments to log
1172
+ */
1173
+ warn(...args) {
1174
+ if (this.currentLevel <= this.levels.warn) {
1175
+ console.warn('[WARN]', ...args);
1176
+ }
1177
+ }
1178
+
1179
+ /**
1180
+ * Error log
1181
+ * @param {...*} args - Arguments to log
1182
+ */
1183
+ error(...args) {
1184
+ if (this.currentLevel <= this.levels.error) {
1185
+ console.error('[ERROR]', ...args);
1186
+ }
1187
+ }
1188
+ }
1189
+
1190
+ // Default logger instance
1191
+ new Logger();
1192
+
1193
+ /**
1194
+ * Time formatting utilities
1195
+ */
1196
+ class TimeUtils {
1197
+ /**
1198
+ * Format seconds to MM:SS or HH:MM:SS
1199
+ * @param {number} seconds - Time in seconds
1200
+ * @returns {string} Formatted time string
1201
+ */
1202
+ static format(seconds) {
1203
+ if (!Number.isFinite(seconds) || seconds < 0) return '00:00';
1204
+
1205
+ const hours = Math.floor(seconds / 3600);
1206
+ const minutes = Math.floor((seconds % 3600) / 60);
1207
+ const secs = Math.floor(seconds % 60);
1208
+
1209
+ if (hours > 0) {
1210
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
1211
+ }
1212
+ return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
1213
+ }
1214
+
1215
+ /**
1216
+ * Parse time string to seconds
1217
+ * @param {string} timeStr - Time string like "1:23" or "01:23:45"
1218
+ * @returns {number} Time in seconds
1219
+ */
1220
+ static parse(timeStr) {
1221
+ if (!timeStr || typeof timeStr !== 'string') return 0;
1222
+
1223
+ const parts = timeStr.split(':').map(Number);
1224
+ if (parts.length === 2) {
1225
+ return parts[0] * 60 + parts[1];
1226
+ } else if (parts.length === 3) {
1227
+ return parts[0] * 3600 + parts[1] * 60 + parts[2];
1228
+ }
1229
+ return 0;
1230
+ }
1231
+
1232
+ /**
1233
+ * Get current timestamp in milliseconds
1234
+ * @returns {number} Current time
1235
+ */
1236
+ static now() {
1237
+ return Date.now();
1238
+ }
1239
+
1240
+ /**
1241
+ * Calculate duration between two timestamps
1242
+ * @param {number} start - Start time
1243
+ * @param {number} end - End time
1244
+ * @returns {number} Duration in milliseconds
1245
+ */
1246
+ static duration(start, end) {
1247
+ return end - start;
1248
+ }
1249
+ }
1250
+
1251
+ /**
1252
+ * Audio probing utilities
1253
+ */
1254
+ class ProbeUtils {
1255
+ /**
1256
+ * Probe audio file/stream for basic info
1257
+ * @param {string|Buffer|ReadableStream} source - Audio source
1258
+ * @returns {Promise<Object>} Probe result
1259
+ */
1260
+ static async probe(source) {
1261
+ // In a real implementation, this would use ffprobe or similar
1262
+ // For now, return basic mock data
1263
+ return {
1264
+ duration: null, // seconds
1265
+ format: null, // e.g., 'mp3', 'wav'
1266
+ bitrate: null, // kbps
1267
+ sampleRate: null, // Hz
1268
+ channels: null // 1 or 2
1269
+ };
1270
+ }
1271
+
1272
+ /**
1273
+ * Check if source is a valid audio URL
1274
+ * @param {string} url - URL to check
1275
+ * @returns {boolean} Is valid audio URL
1276
+ */
1277
+ static isValidAudioUrl(url) {
1278
+ if (!url || typeof url !== 'string') return false;
1279
+
1280
+ try {
1281
+ const parsed = new URL(url);
1282
+ const audioExtensions = ['.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a'];
1283
+ const path = parsed.pathname.toLowerCase();
1284
+
1285
+ return audioExtensions.some(ext => path.endsWith(ext)) ||
1286
+ url.includes('youtube.com') ||
1287
+ url.includes('youtu.be') ||
1288
+ url.includes('soundcloud.com');
1289
+ } catch {
1290
+ return false;
1291
+ }
1292
+ }
1293
+
1294
+ /**
1295
+ * Get audio format from URL or buffer
1296
+ * @param {string|Buffer} source - Audio source
1297
+ * @returns {string|null} Audio format
1298
+ */
1299
+ static getFormat(source) {
1300
+ if (typeof source === 'string') {
1301
+ const extensions = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'];
1302
+ for (const ext of extensions) {
1303
+ if (source.toLowerCase().includes(`.${ext}`)) {
1304
+ return ext;
1305
+ }
1306
+ }
1307
+ }
1308
+ // For buffer, would need to check headers
1309
+ return null;
1310
+ }
1311
+ }
1312
+
1313
+ /**
1314
+ * YouTube provider for basic info fetching
1315
+ */
1316
+ class YouTubeProvider {
1317
+ /**
1318
+ * Check if URL is a valid YouTube URL
1319
+ * @param {string} url - URL to check
1320
+ * @returns {boolean} Is valid YouTube URL
1321
+ */
1322
+ static isValidUrl(url) {
1323
+ return url.includes('youtube.com/watch') || url.includes('youtu.be/');
1324
+ }
1325
+
1326
+ /**
1327
+ * Extract video ID from YouTube URL
1328
+ * @param {string} url - YouTube URL
1329
+ * @returns {string|null} Video ID
1330
+ */
1331
+ static extractVideoId(url) {
1332
+ try {
1333
+ const urlObj = new URL(url);
1334
+ if (urlObj.hostname === 'youtu.be') {
1335
+ return urlObj.pathname.slice(1);
1336
+ }
1337
+ return urlObj.searchParams.get('v');
1338
+ } catch {
1339
+ return null;
1340
+ }
1341
+ }
1342
+
1343
+ /**
1344
+ * Get basic track info from YouTube URL
1345
+ * @param {string} url - YouTube URL
1346
+ * @returns {Promise<Object>} Track info
1347
+ */
1348
+ static async getInfo(url) {
1349
+ const videoId = this.extractVideoId(url);
1350
+ if (!videoId) {
1351
+ throw new Error('Invalid YouTube URL');
1352
+ }
1353
+
1354
+ // In a real implementation, this would call YouTube API
1355
+ // For now, return mock data
1356
+ return {
1357
+ title: `YouTube Video ${videoId}`,
1358
+ artist: null,
1359
+ duration: null,
1360
+ thumbnail: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
1361
+ url: url,
1362
+ source: 'youtube',
1363
+ videoId: videoId
1364
+ };
1365
+ }
1366
+
1367
+ /**
1368
+ * Get stream URL (not implemented without dependencies)
1369
+ * @param {string} url - YouTube URL
1370
+ * @returns {Promise<string>} Stream URL
1371
+ */
1372
+ static async getStreamUrl(url) {
1373
+ // Would require ytdl-core or similar
1374
+ throw new Error('Stream URL extraction requires additional dependencies');
1375
+ }
1376
+ }
1377
+
1378
+ /**
1379
+ * SoundCloud provider for basic info fetching
1380
+ */
1381
+ class SoundCloudProvider {
1382
+ /**
1383
+ * Check if URL is a valid SoundCloud URL
1384
+ * @param {string} url - URL to check
1385
+ * @returns {boolean} Is valid SoundCloud URL
1386
+ */
1387
+ static isValidUrl(url) {
1388
+ return url.includes('soundcloud.com/');
1389
+ }
1390
+
1391
+ /**
1392
+ * Get basic track info from SoundCloud URL
1393
+ * @param {string} url - SoundCloud URL
1394
+ * @returns {Promise<Object>} Track info
1395
+ */
1396
+ static async getInfo(url) {
1397
+ // In a real implementation, this would call SoundCloud API
1398
+ // For now, return mock data
1399
+ return {
1400
+ title: 'SoundCloud Track',
1401
+ artist: null,
1402
+ duration: null,
1403
+ thumbnail: null,
1404
+ url: url,
1405
+ source: 'soundcloud'
1406
+ };
1407
+ }
1408
+
1409
+ /**
1410
+ * Get stream URL (not implemented without dependencies)
1411
+ * @param {string} url - SoundCloud URL
1412
+ * @returns {Promise<string>} Stream URL
1413
+ */
1414
+ static async getStreamUrl(url) {
1415
+ // Would require soundcloud-scraper or similar
1416
+ throw new Error('Stream URL extraction requires additional dependencies');
1417
+ }
1418
+ }
1419
+
1420
+ /**
1421
+ * Local file provider for Node.js
1422
+ */
1423
+ class LocalProvider {
1424
+ /**
1425
+ * Check if path is a valid local audio file
1426
+ * @param {string} path - File path
1427
+ * @returns {boolean} Is valid audio file
1428
+ */
1429
+ static isValidPath(path) {
1430
+ const audioExtensions = ['.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a'];
1431
+ return audioExtensions.includes(extname(path).toLowerCase());
1432
+ }
1433
+
1434
+ /**
1435
+ * Get track info from local file
1436
+ * @param {string} path - File path
1437
+ * @returns {Promise<Object>} Track info
1438
+ */
1439
+ static async getInfo(path) {
1440
+ try {
1441
+ const stats = await promises.stat(path);
1442
+ if (!stats.isFile()) {
1443
+ throw new Error('Path is not a file');
1444
+ }
1445
+
1446
+ return {
1447
+ title: path.split('/').pop().replace(extname(path), ''),
1448
+ artist: null,
1449
+ duration: null, // Would need audio parsing library
1450
+ thumbnail: null,
1451
+ url: `file://${path}`,
1452
+ source: 'local',
1453
+ size: stats.size,
1454
+ modified: stats.mtime
1455
+ };
1456
+ } catch (error) {
1457
+ throw new Error(`Failed to get file info: ${error.message}`);
1458
+ }
1459
+ }
1460
+
1461
+ /**
1462
+ * Check if file exists
1463
+ * @param {string} path - File path
1464
+ * @returns {Promise<boolean>} File exists
1465
+ */
1466
+ static async exists(path) {
1467
+ try {
1468
+ await promises.access(path);
1469
+ return true;
1470
+ } catch {
1471
+ return false;
1472
+ }
1473
+ }
1474
+ }
1475
+
1476
+ export { AudioEngine, EVENTS, EventBus, FILTER_TYPES, LOOP_MODES$1 as LOOP_MODES, LocalProvider, Logger, MetadataUtils, PluginManager, ProbeUtils, Queue, REPEAT_MODES, SoundCloudProvider, TimeUtils, Track, YouTubeProvider };
1477
+ //# sourceMappingURL=index.js.map