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