loukai-app 0.3.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.
- package/README.md +558 -0
- package/bin/loukai.js +32 -0
- package/package.json +243 -0
- package/src/main/appState.js +250 -0
- package/src/main/audioEngine.js +478 -0
- package/src/main/creator/conversionService.js +503 -0
- package/src/main/creator/downloadManager.js +1128 -0
- package/src/main/creator/ffmpegService.js +487 -0
- package/src/main/creator/installLogger.js +51 -0
- package/src/main/creator/keyDetection.js +212 -0
- package/src/main/creator/llmService.js +370 -0
- package/src/main/creator/lrclibService.js +340 -0
- package/src/main/creator/python/crepe_runner.py +189 -0
- package/src/main/creator/python/demucs_runner.py +158 -0
- package/src/main/creator/python/whisper_runner.py +172 -0
- package/src/main/creator/pythonRunner.js +268 -0
- package/src/main/creator/stemBuilder.js +491 -0
- package/src/main/creator/systemChecker.js +474 -0
- package/src/main/handlers/appHandlers.js +45 -0
- package/src/main/handlers/audioHandlers.js +33 -0
- package/src/main/handlers/autotuneHandlers.js +28 -0
- package/src/main/handlers/canvasHandlers.js +84 -0
- package/src/main/handlers/creatorHandlers.js +159 -0
- package/src/main/handlers/editorHandlers.js +98 -0
- package/src/main/handlers/effectsHandlers.js +100 -0
- package/src/main/handlers/fileHandlers.js +45 -0
- package/src/main/handlers/index.js +78 -0
- package/src/main/handlers/libraryHandlers.js +96 -0
- package/src/main/handlers/mixerHandlers.js +64 -0
- package/src/main/handlers/playerHandlers.js +39 -0
- package/src/main/handlers/preferencesHandlers.js +46 -0
- package/src/main/handlers/queueHandlers.js +81 -0
- package/src/main/handlers/rendererHandlers.js +63 -0
- package/src/main/handlers/settingsHandlers.js +42 -0
- package/src/main/handlers/webServerHandlers.js +105 -0
- package/src/main/main.js +2351 -0
- package/src/main/preload.js +252 -0
- package/src/main/settingsManager.js +139 -0
- package/src/main/statePersistence.js +193 -0
- package/src/main/utils/pathValidator.js +112 -0
- package/src/main/webServer.js +2535 -0
- package/src/native/autotune.js +417 -0
- package/src/renderer/adapters/ElectronBridge.js +677 -0
- package/src/renderer/canvas.html +80 -0
- package/src/renderer/components/App.jsx +303 -0
- package/src/renderer/components/AppRoot.jsx +37 -0
- package/src/renderer/components/AudioDeviceSettings.jsx +145 -0
- package/src/renderer/components/EffectsPanelWrapper.jsx +267 -0
- package/src/renderer/components/MixerTab.jsx +233 -0
- package/src/renderer/components/MixerTabWrapper.jsx +31 -0
- package/src/renderer/components/PortalSelect.jsx +239 -0
- package/src/renderer/components/QueueTab.jsx +116 -0
- package/src/renderer/components/RequestsListWrapper.jsx +78 -0
- package/src/renderer/components/ServerTab.jsx +472 -0
- package/src/renderer/components/SongInfoBarWrapper.jsx +77 -0
- package/src/renderer/components/StatusBar.jsx +92 -0
- package/src/renderer/components/TabNavigation.jsx +77 -0
- package/src/renderer/components/TransportControlsWrapper.jsx +69 -0
- package/src/renderer/components/creator/CreateTab.jsx +1236 -0
- package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js +2 -0
- package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js.map +1 -0
- package/src/renderer/dist/assets/microphoneEngine-BaCUhhQc.js +2 -0
- package/src/renderer/dist/assets/microphoneEngine-BaCUhhQc.js.map +1 -0
- package/src/renderer/dist/assets/player-DVrqp7N5.js +3 -0
- package/src/renderer/dist/assets/player-DVrqp7N5.js.map +1 -0
- package/src/renderer/dist/assets/songLoaders-BaTgGib4.js +2 -0
- package/src/renderer/dist/assets/songLoaders-BaTgGib4.js.map +1 -0
- package/src/renderer/dist/assets/webrtcManager-BhCHWceK.js +2 -0
- package/src/renderer/dist/assets/webrtcManager-BhCHWceK.js.map +1 -0
- package/src/renderer/dist/js/autoTuneWorklet.js +224 -0
- package/src/renderer/dist/js/micPitchDetectorWorklet.js +137 -0
- package/src/renderer/dist/js/musicAnalysisWorklet.js +216 -0
- package/src/renderer/dist/js/phaseVocoderWorklet.js +341 -0
- package/src/renderer/dist/js/soundtouch-worklet.js +1395 -0
- package/src/renderer/dist/renderer.css +1 -0
- package/src/renderer/dist/renderer.js +62 -0
- package/src/renderer/dist/renderer.js.map +1 -0
- package/src/renderer/dist/renderer.woff2 +0 -0
- package/src/renderer/hooks/useKeyboardShortcuts.js +154 -0
- package/src/renderer/index.html +24 -0
- package/src/renderer/index.html.backup +372 -0
- package/src/renderer/js/PlayerInterface.js +267 -0
- package/src/renderer/js/autoTuneWorklet.js +224 -0
- package/src/renderer/js/butterchurnVerify.js +46 -0
- package/src/renderer/js/canvas-app.js +114 -0
- package/src/renderer/js/cdgPlayer.js +685 -0
- package/src/renderer/js/kaiPlayer.js +1200 -0
- package/src/renderer/js/karaokeRenderer.js +3392 -0
- package/src/renderer/js/micPitchDetectorWorklet.js +137 -0
- package/src/renderer/js/microphoneEngine.js +656 -0
- package/src/renderer/js/musicAnalysisWorklet.js +216 -0
- package/src/renderer/js/phaseVocoderWorklet.js +341 -0
- package/src/renderer/js/player.js +232 -0
- package/src/renderer/js/referencePitchTracker.js +130 -0
- package/src/renderer/js/songLoaders.js +334 -0
- package/src/renderer/js/soundtouch-worklet.js +1395 -0
- package/src/renderer/js/webrtcManager.js +511 -0
- package/src/renderer/lib/butterchurn.min.js +6739 -0
- package/src/renderer/lib/butterchurnPresets.min.js +1 -0
- package/src/renderer/lib/cdgraphics-wrapper.js +16 -0
- package/src/renderer/lib/cdgraphics.js +299 -0
- package/src/renderer/public/js/autoTuneWorklet.js +224 -0
- package/src/renderer/public/js/micPitchDetectorWorklet.js +137 -0
- package/src/renderer/public/js/musicAnalysisWorklet.js +216 -0
- package/src/renderer/public/js/phaseVocoderWorklet.js +341 -0
- package/src/renderer/public/js/soundtouch-worklet.js +1395 -0
- package/src/renderer/react-entry.jsx +44 -0
- package/src/renderer/styles/tailwind.css +106 -0
- package/src/renderer/utils/qrCodeGenerator.js +98 -0
- package/src/renderer/vite.config.js +31 -0
- package/src/shared/adapters/BridgeInterface.js +195 -0
- package/src/shared/components/EffectsPanel.jsx +177 -0
- package/src/shared/components/LibraryPanel.jsx +701 -0
- package/src/shared/components/LineDetailCanvas.jsx +167 -0
- package/src/shared/components/LyricLine.jsx +505 -0
- package/src/shared/components/LyricRejection.jsx +84 -0
- package/src/shared/components/LyricSuggestion.jsx +80 -0
- package/src/shared/components/LyricsEditorCanvas.jsx +271 -0
- package/src/shared/components/MixerPanel.jsx +94 -0
- package/src/shared/components/PlayerControls.jsx +206 -0
- package/src/shared/components/PortalSelect.jsx +239 -0
- package/src/shared/components/QueueList.jsx +365 -0
- package/src/shared/components/QuickSearch.jsx +126 -0
- package/src/shared/components/RequestsList.jsx +121 -0
- package/src/shared/components/SongEditor.jsx +1362 -0
- package/src/shared/components/SongInfoBar.jsx +81 -0
- package/src/shared/components/ThemeToggle.jsx +106 -0
- package/src/shared/components/Toast.jsx +30 -0
- package/src/shared/components/VisualizationSettings.jsx +243 -0
- package/src/shared/constants.js +95 -0
- package/src/shared/context/BridgeContext.jsx +32 -0
- package/src/shared/contexts/AudioContext.jsx +37 -0
- package/src/shared/contexts/PlayerContext.jsx +66 -0
- package/src/shared/contexts/SettingsContext.jsx +50 -0
- package/src/shared/defaults.js +158 -0
- package/src/shared/formatUtils.js +59 -0
- package/src/shared/formatUtils.test.js +207 -0
- package/src/shared/hooks/useAppState.js +97 -0
- package/src/shared/hooks/useAudioEngine.js +264 -0
- package/src/shared/hooks/usePlayer.js +89 -0
- package/src/shared/hooks/useSettingsPersistence.js +74 -0
- package/src/shared/hooks/useWebRTC.js +118 -0
- package/src/shared/ipcContracts.js +299 -0
- package/src/shared/package.json +3 -0
- package/src/shared/services/creatorService.js +373 -0
- package/src/shared/services/creatorService.test.js +413 -0
- package/src/shared/services/editorService.js +213 -0
- package/src/shared/services/editorService.test.js +219 -0
- package/src/shared/services/effectsService.js +271 -0
- package/src/shared/services/effectsService.test.js +418 -0
- package/src/shared/services/libraryService.js +438 -0
- package/src/shared/services/libraryService.test.js +474 -0
- package/src/shared/services/mixerService.js +172 -0
- package/src/shared/services/mixerService.test.js +399 -0
- package/src/shared/services/playerService.js +221 -0
- package/src/shared/services/playerService.test.js +357 -0
- package/src/shared/services/preferencesService.js +219 -0
- package/src/shared/services/queueService.js +226 -0
- package/src/shared/services/queueService.test.js +430 -0
- package/src/shared/services/requestsService.js +155 -0
- package/src/shared/services/requestsService.test.js +362 -0
- package/src/shared/services/serverSettingsService.js +151 -0
- package/src/shared/services/settingsService.js +257 -0
- package/src/shared/services/settingsService.test.js +295 -0
- package/src/shared/state/StateManager.js +263 -0
- package/src/shared/utils/audio.js +42 -0
- package/src/shared/utils/format.js +32 -0
- package/src/shared/utils/lyricsUtils.js +162 -0
- package/src/test/setup.js +40 -0
- package/src/utils/cdgLoader.js +180 -0
- package/src/utils/m4aLoader.js +333 -0
- package/src/web/App.jsx +578 -0
- package/src/web/adapters/WebBridge.js +428 -0
- package/src/web/components/PlayerSettingsPanel.jsx +231 -0
- package/src/web/components/SongSearch.jsx +180 -0
- package/src/web/dist/assets/index-0H-RnRrV.js +51 -0
- package/src/web/dist/assets/index-0H-RnRrV.js.map +1 -0
- package/src/web/dist/assets/index-DYW2zB0u.css +1 -0
- package/src/web/dist/index.html +15 -0
- package/src/web/index.html +14 -0
- package/src/web/main.jsx +10 -0
- package/src/web/package-lock.json +1765 -0
- package/src/web/pages/SongRequestPage.jsx +619 -0
- package/src/web/styles/tailwind.css +68 -0
- package/src/web/vite.config.js +27 -0
- package/static/fonts/material-icons.woff2 +0 -0
- package/static/images/butterchurn-screenshots/Aderrasi - Potion of Spirits.png +0 -0
- package/static/images/butterchurn-screenshots/Aderrasi - Songflower _Moss Posy_.png +0 -0
- package/static/images/butterchurn-screenshots/Aderrasi - Storm of the Eye _Thunder_ - mash0000 - quasi pseudo meta concentrics.png +0 -0
- package/static/images/butterchurn-screenshots/Aderrasi _ Geiss - Airhandler _Kali Mix_ - Canvas Mix.png +0 -0
- package/static/images/butterchurn-screenshots/An AdamFX n Martin Infusion 2 flexi - Why The Sky Looks Diffrent Today - AdamFx n Martin Infusion - Tack Tile Disfunction B.png +0 -0
- package/static/images/butterchurn-screenshots/Cope - The Neverending Explosion of Red Liquid Fire.png +0 -0
- proton lights __Krash_s beat code_ _Phat_remix02b.png +0 -0
- package/static/images/butterchurn-screenshots/Eo_S_ _ Phat - cubetrace - v2.png +0 -0
- package/static/images/butterchurn-screenshots/Eo_S_ _ Zylot - skylight _Stained Glass Majesty mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - alien fish pond.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - area 51.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - infused with the spiral.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - mindblob _shiny mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - mindblob mix.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - predator-prey-spirals.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - smashing fractals _acid etching mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - truly soft piece of software - this is generic texturing _Jelly_ .png +0 -0
- package/static/images/butterchurn-screenshots/Flexi _ Martin - astral projection.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi _ Martin - cascading decay swing.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi _ amandio c - piercing 05 - Kopie _2_ - Kopie.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi _ stahlregen - jelly showoff parade.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi_ fishbrain_ Geiss _ Martin - tokamak witchery.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi_ martin _ geiss - dedicated to the sherwin maxawow.png +0 -0
- package/static/images/butterchurn-screenshots/Fumbling_Foo _ Flexi_ Martin_ Orb_ Unchained - Star Nova v7b.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss - Cauldron - painterly 2 _saturation remix_.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss - Reaction Diffusion 2.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss - Spiral Artifact.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss - Thumb Drum.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss _ Flexi _ Martin - disconnected.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss_ Flexi _ Stahlregen - Thumbdrum Tokamak _crossfiring aftermath jelly mashup_.png +0 -0
- package/static/images/butterchurn-screenshots/Goody - The Wild Vort.png +0 -0
- package/static/images/butterchurn-screenshots/Idiot - Star Of Annon.png +0 -0
- package/static/images/butterchurn-screenshots/Krash _ Illusion - Spiral Movement.png +0 -0
- package/static/images/butterchurn-screenshots/Martin - QBikal - Surface Turbulence IIb.png +0 -0
- package/static/images/butterchurn-screenshots/Martin - acid wiring.png +0 -0
- package/static/images/butterchurn-screenshots/Martin - charisma.png +0 -0
- package/static/images/butterchurn-screenshots/Martin - liquid arrows.png +0 -0
- package/static/images/butterchurn-screenshots/Milk Artist At our Best - FED - SlowFast Ft AdamFX n Martin - HD CosmoFX.png +0 -0
- package/static/images/butterchurn-screenshots/ORB - Waaa.png +0 -0
- package/static/images/butterchurn-screenshots/Phat_fiShbRaiN_Eo_S_Mandala_Chasers_remix.png +0 -0
- package/static/images/butterchurn-screenshots/Rovastar - Oozing Resistance.png +0 -0
- package/static/images/butterchurn-screenshots/Rovastar _ Loadus _ Geiss - FractalDrop _Triple Mix_.png +0 -0
- package/static/images/butterchurn-screenshots/TonyMilkdrop - Leonardo Da Vinci_s Balloon _Flexi - merry-go-round _ techstyle_.png +0 -0
- package/static/images/butterchurn-screenshots/TonyMilkdrop - Magellan_s Nebula _Flexi - you enter first _ multiverse_.png +0 -0
- package/static/images/butterchurn-screenshots/Unchained - Rewop.png +0 -0
- package/static/images/butterchurn-screenshots/Unchained - Unified Drag 2.png +0 -0
- package/static/images/butterchurn-screenshots/Unchained _ Rovastar - Wormhole Pillars _Hall of Shadows mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Zylot - Paint Spill _Music Reactive Paint Mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Zylot - Star Ornament.png +0 -0
- package/static/images/butterchurn-screenshots/Zylot - True Visionary _Final Mix_.png +0 -0
- package/static/images/butterchurn-screenshots/_Aderrasi - Wanderer in Curved Space - mash0000 - faclempt kibitzing meshuggana schmaltz _Geiss color mix_.png +0 -0
- package/static/images/butterchurn-screenshots/_Geiss - Artifact 01.png +0 -0
- package/static/images/butterchurn-screenshots/_Geiss - Desert Rose 2.png +0 -0
- package/static/images/butterchurn-screenshots/_Geiss - untitled.png +0 -0
- package/static/images/butterchurn-screenshots/_Mig_049.png +0 -0
- package/static/images/butterchurn-screenshots/_Mig_085.png +0 -0
- package/static/images/butterchurn-screenshots/_Rovastar _ Geiss - Hurricane Nightmare _Posterize Mix_.png +0 -0
- package/static/images/butterchurn-screenshots/___ Royal - Mashup _197_.png +0 -0
- package/static/images/butterchurn-screenshots/___ Royal - Mashup _220_.png +0 -0
- package/static/images/butterchurn-screenshots/___ Royal - Mashup _431_.png +0 -0
- package/static/images/butterchurn-screenshots/cope _ martin - mother-of-pearl.png +0 -0
- package/static/images/butterchurn-screenshots/fiShbRaiN _ Flexi - witchcraft 2_0.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - bouncing balls _double mindblob neon mix_.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - mom_ why the sky looks different today.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - patternton_ district of media_ capitol of the united abstractions of fractopia.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - swing out on the spiral.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - what is the matrix.png +0 -0
- package/static/images/butterchurn-screenshots/flexi _ amandio c - organic _random mashup_.png +0 -0
- package/static/images/butterchurn-screenshots/flexi _ amandio c - organic12-3d-2_milk.png +0 -0
- package/static/images/butterchurn-screenshots/flexi _ fishbrain - neon mindblob grafitti.png +0 -0
- package/static/images/butterchurn-screenshots/flexi _ geiss - pogo cubes vs_ tokamak vs_ game of life _stahls jelly 4_5 finish_.png +0 -0
- package/static/images/butterchurn-screenshots/high-altitude basket unraveling - singh grooves nitrogen argon nz_.png +0 -0
- package/static/images/butterchurn-screenshots/martin - The Bridge of Khazad-Dum.png +0 -0
- package/static/images/butterchurn-screenshots/martin - angel flight.png +0 -0
- package/static/images/butterchurn-screenshots/martin - another kind of groove.png +0 -0
- package/static/images/butterchurn-screenshots/martin - bombyx mori.png +0 -0
- package/static/images/butterchurn-screenshots/martin - castle in the air.png +0 -0
- package/static/images/butterchurn-screenshots/martin - chain breaker.png +0 -0
- package/static/images/butterchurn-screenshots/martin - disco mix 4.png +0 -0
- package/static/images/butterchurn-screenshots/martin - extreme heat.png +0 -0
- package/static/images/butterchurn-screenshots/martin - frosty caves 2.png +0 -0
- package/static/images/butterchurn-screenshots/martin - fruit machine.png +0 -0
- package/static/images/butterchurn-screenshots/martin - ghost city.png +0 -0
- package/static/images/butterchurn-screenshots/martin - glass corridor.png +0 -0
- package/static/images/butterchurn-screenshots/martin - infinity _2010 update_.png +0 -0
- package/static/images/butterchurn-screenshots/martin - mandelbox explorer - high speed demo version.png +0 -0
- package/static/images/butterchurn-screenshots/martin - mucus cervix.png +0 -0
- package/static/images/butterchurn-screenshots/martin - reflections on black tiles.png +0 -0
- package/static/images/butterchurn-screenshots/martin - stormy sea _2010 update_.png +0 -0
- package/static/images/butterchurn-screenshots/martin - witchcraft reloaded.png +0 -0
- package/static/images/butterchurn-screenshots/martin _ flexi - diamond cutter _prismaticvortex_com_ - camille - i wish i wish i wish i was constrained.png +0 -0
- package/static/images/butterchurn-screenshots/martin _shadow harlequins shape code_ - fata morgana.png +0 -0
- package/static/images/butterchurn-screenshots/martin_ flexi_ fishbrain _ sto - enterstate _random mashup_.png +0 -0
- package/static/images/butterchurn-screenshots/sawtooth grin roam.png +0 -0
- package/static/images/butterchurn-screenshots/shifter - dark tides bdrv mix 2.png +0 -0
- package/static/images/butterchurn-screenshots/suksma - Rovastar - Sunflower Passion _Enlightment Mix__Phat_edit _ flexi und martin shaders - circumflex in character classes in regular expression.png +0 -0
- package/static/images/butterchurn-screenshots/suksma - heretical crosscut playpen.png +0 -0
- package/static/images/butterchurn-screenshots/suksma - uninitialized variabowl _hydroponic chronic_.png +0 -0
- package/static/images/butterchurn-screenshots/suksma - vector exp 1 - couldn_t not.png +0 -0
- package/static/images/butterchurn-screenshots/yin - 191 - Temporal singularities.png +0 -0
- package/static/images/logo-512.png +0 -0
- package/static/images/logo.png +0 -0
- package/static/loukai-logo.png +0 -0
- package/static/screenshot-generator.html +610 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
// CDGraphics will be loaded from node_modules via webpack or as a global
|
|
2
|
+
// For now, we'll load it dynamically when needed
|
|
3
|
+
|
|
4
|
+
/* global CDGraphics */
|
|
5
|
+
|
|
6
|
+
import { PlayerInterface } from './PlayerInterface.js';
|
|
7
|
+
import { MicrophoneEngine } from './microphoneEngine.js';
|
|
8
|
+
|
|
9
|
+
export class CDGPlayer extends PlayerInterface {
|
|
10
|
+
constructor(canvasId) {
|
|
11
|
+
super(); // Call PlayerInterface constructor
|
|
12
|
+
|
|
13
|
+
this.canvas = document.getElementById(canvasId);
|
|
14
|
+
|
|
15
|
+
if (!this.canvas) {
|
|
16
|
+
console.error('CDG canvas not found:', canvasId);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
this.ctx = this.canvas.getContext('2d');
|
|
21
|
+
this.cdgPlayer = null;
|
|
22
|
+
this.cdgData = null;
|
|
23
|
+
// Note: this.isPlaying is inherited from PlayerInterface
|
|
24
|
+
this.currentTime = 0;
|
|
25
|
+
this.animationFrame = null;
|
|
26
|
+
|
|
27
|
+
// CDG output canvas (300x216)
|
|
28
|
+
this.cdgCanvas = document.createElement('canvas');
|
|
29
|
+
this.cdgCanvas.width = 300;
|
|
30
|
+
this.cdgCanvas.height = 216;
|
|
31
|
+
this.cdgCtx = this.cdgCanvas.getContext('2d');
|
|
32
|
+
|
|
33
|
+
// Web Audio API for MP3 playback (will be set by main.js)
|
|
34
|
+
this.audioContext = null;
|
|
35
|
+
this.audioSource = null;
|
|
36
|
+
this.audioBuffer = null;
|
|
37
|
+
this.startTime = 0;
|
|
38
|
+
this.pauseTime = 0;
|
|
39
|
+
this.gainNode = null;
|
|
40
|
+
this.analyserNode = null;
|
|
41
|
+
|
|
42
|
+
// Background effects (Butterchurn)
|
|
43
|
+
this.effectsCanvas = null;
|
|
44
|
+
this.butterchurn = null;
|
|
45
|
+
this.effectsEnabled = true;
|
|
46
|
+
this.overlayOpacity = 0.7; // Default, will be updated from settings
|
|
47
|
+
|
|
48
|
+
// Microphone engine (handles mic input and auto-tune)
|
|
49
|
+
this.micEngine = null; // Will be initialized when audio context is set
|
|
50
|
+
|
|
51
|
+
// QR code for server URL
|
|
52
|
+
this.qrCodeCanvas = null;
|
|
53
|
+
this.showQrCode = false;
|
|
54
|
+
this.serverUrl = null;
|
|
55
|
+
|
|
56
|
+
// Queue display
|
|
57
|
+
this.queueItems = [];
|
|
58
|
+
this.displayQueue = true;
|
|
59
|
+
|
|
60
|
+
// Note: this.stateReportInterval is inherited from PlayerInterface
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Set server URL and generate QR code
|
|
65
|
+
* @param {string} url - Server URL
|
|
66
|
+
* @param {boolean} show - Whether to show QR code
|
|
67
|
+
*/
|
|
68
|
+
async setServerQRCode(url, show) {
|
|
69
|
+
this.serverUrl = url;
|
|
70
|
+
this.showQrCode = show;
|
|
71
|
+
|
|
72
|
+
if (url && show) {
|
|
73
|
+
try {
|
|
74
|
+
// Dynamically import QR code generator
|
|
75
|
+
const { generateQRCodeCanvas } = await import('../utils/qrCodeGenerator.js');
|
|
76
|
+
this.qrCodeCanvas = await generateQRCodeCanvas(url, 150);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Error generating QR code:', error);
|
|
79
|
+
this.qrCodeCanvas = null;
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
this.qrCodeCanvas = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Set queue items and display setting
|
|
88
|
+
* @param {Array} queue - Array of queue items with title, artist, requester
|
|
89
|
+
* @param {boolean} display - Whether to display queue
|
|
90
|
+
*/
|
|
91
|
+
setQueueDisplay(queue, display) {
|
|
92
|
+
this.queueItems = queue || [];
|
|
93
|
+
this.displayQueue = display !== false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setOverlayOpacity(opacity) {
|
|
97
|
+
this.overlayOpacity = opacity;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Implements PlayerInterface.loadSong()
|
|
102
|
+
* @param {Object} songData - CDG song data
|
|
103
|
+
* @returns {Promise<boolean>} Success status
|
|
104
|
+
*/
|
|
105
|
+
async loadSong(_songData) {
|
|
106
|
+
try {
|
|
107
|
+
this.cdgData = _songData;
|
|
108
|
+
|
|
109
|
+
// Reset position using base class method
|
|
110
|
+
this.resetPosition();
|
|
111
|
+
|
|
112
|
+
// Reset CDG-specific timing state
|
|
113
|
+
this.currentTime = 0;
|
|
114
|
+
this.startTime = 0;
|
|
115
|
+
this.pauseTime = 0;
|
|
116
|
+
|
|
117
|
+
// Load CDGraphics library dynamically
|
|
118
|
+
if (typeof CDGraphics === 'undefined') {
|
|
119
|
+
console.error('💿 CDGraphics library not loaded');
|
|
120
|
+
throw new Error('CDGraphics library not available');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Load CDG file data - convert to ArrayBuffer first
|
|
124
|
+
const cdgBuffer = _songData.cdg.data;
|
|
125
|
+
|
|
126
|
+
// Convert to ArrayBuffer
|
|
127
|
+
let arrayBuffer;
|
|
128
|
+
if (cdgBuffer instanceof Uint8Array || cdgBuffer instanceof Buffer) {
|
|
129
|
+
// Create a new ArrayBuffer and copy data
|
|
130
|
+
arrayBuffer = new ArrayBuffer(cdgBuffer.length || cdgBuffer.byteLength);
|
|
131
|
+
const view = new Uint8Array(arrayBuffer);
|
|
132
|
+
view.set(cdgBuffer);
|
|
133
|
+
} else if (cdgBuffer.buffer instanceof ArrayBuffer) {
|
|
134
|
+
// It's a typed array with an ArrayBuffer
|
|
135
|
+
arrayBuffer = cdgBuffer.buffer.slice(
|
|
136
|
+
cdgBuffer.byteOffset,
|
|
137
|
+
cdgBuffer.byteOffset + cdgBuffer.byteLength
|
|
138
|
+
);
|
|
139
|
+
} else if (cdgBuffer instanceof ArrayBuffer) {
|
|
140
|
+
// It's already an ArrayBuffer
|
|
141
|
+
arrayBuffer = cdgBuffer;
|
|
142
|
+
} else {
|
|
143
|
+
console.error('💿 Unknown buffer type:', cdgBuffer);
|
|
144
|
+
throw new Error('Unknown buffer type');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Initialize CDGraphics player with the ArrayBuffer
|
|
148
|
+
this.cdgPlayer = new CDGraphics(arrayBuffer);
|
|
149
|
+
|
|
150
|
+
// Decode MP3 audio buffer using Web Audio API
|
|
151
|
+
// Audio context will be set by main.js before loading
|
|
152
|
+
if (!this.audioContext) {
|
|
153
|
+
throw new Error('Audio context not set. Call setAudioContext() first.');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const mp3ArrayBuffer = _songData.audio.mp3.buffer.slice(
|
|
157
|
+
_songData.audio.mp3.byteOffset,
|
|
158
|
+
_songData.audio.mp3.byteOffset + _songData.audio.mp3.byteLength
|
|
159
|
+
);
|
|
160
|
+
this.audioBuffer = await this.audioContext.decodeAudioData(mp3ArrayBuffer);
|
|
161
|
+
|
|
162
|
+
return true;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('💿 Failed to load CDG:', error);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
play() {
|
|
170
|
+
if (!this.cdgPlayer || !this.audioBuffer) {
|
|
171
|
+
console.warn('💿 No CDG loaded');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.isPlaying = true;
|
|
176
|
+
|
|
177
|
+
// Stop existing source if any (and clear its onended handler)
|
|
178
|
+
if (this.audioSource) {
|
|
179
|
+
this.audioSource.onended = null; // Clear handler before stopping
|
|
180
|
+
try {
|
|
181
|
+
this.audioSource.stop();
|
|
182
|
+
} catch {
|
|
183
|
+
// Already stopped
|
|
184
|
+
}
|
|
185
|
+
this.audioSource = null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Create new audio source
|
|
189
|
+
this.audioSource = this.audioContext.createBufferSource();
|
|
190
|
+
this.audioSource.buffer = this.audioBuffer;
|
|
191
|
+
|
|
192
|
+
// Connect to gain node (which is connected to PA output)
|
|
193
|
+
this.audioSource.connect(this.gainNode);
|
|
194
|
+
|
|
195
|
+
// Also connect to analyser for Butterchurn
|
|
196
|
+
if (this.analyserNode) {
|
|
197
|
+
this.audioSource.connect(this.analyserNode);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Connect to microphone engine for real-time music pitch detection (auto-tune)
|
|
201
|
+
if (this.micEngine) {
|
|
202
|
+
this.micEngine.connectMusicSource(this.audioSource);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Handle song end - check both isPlaying AND that we've reached the end naturally
|
|
206
|
+
const duration = this.audioBuffer.duration;
|
|
207
|
+
this.audioSource.onended = () => {
|
|
208
|
+
const currentPos = this.getCurrentTime();
|
|
209
|
+
// Only treat as ended if we're near the end of the song (within 1 second)
|
|
210
|
+
if (this.isPlaying && currentPos >= duration - 1) {
|
|
211
|
+
this.handleSongEnd();
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Start playback from current position
|
|
216
|
+
const offset = this.pauseTime || 0;
|
|
217
|
+
this.audioSource.start(0, offset);
|
|
218
|
+
this.startTime = this.audioContext.currentTime - offset;
|
|
219
|
+
|
|
220
|
+
this.startRendering();
|
|
221
|
+
|
|
222
|
+
// Start state reporting
|
|
223
|
+
this.startStateReporting();
|
|
224
|
+
|
|
225
|
+
// Update microphone engine playing state
|
|
226
|
+
if (this.micEngine) {
|
|
227
|
+
this.micEngine.setPlaying(true);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Report immediate state change
|
|
231
|
+
this.reportStateChange();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
pause() {
|
|
235
|
+
this.isPlaying = false;
|
|
236
|
+
|
|
237
|
+
// Store current position before stopping
|
|
238
|
+
this.pauseTime = this.getCurrentTime();
|
|
239
|
+
|
|
240
|
+
// Stop state reporting
|
|
241
|
+
this.stopStateReporting();
|
|
242
|
+
|
|
243
|
+
// Update microphone engine playing state
|
|
244
|
+
if (this.micEngine) {
|
|
245
|
+
this.micEngine.setPlaying(false);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Report paused state
|
|
249
|
+
this.reportStateChange();
|
|
250
|
+
|
|
251
|
+
// Stop audio source (and clear onended handler to prevent false song-end events)
|
|
252
|
+
if (this.audioSource) {
|
|
253
|
+
this.audioSource.onended = null; // Clear handler first
|
|
254
|
+
|
|
255
|
+
// Disconnect from music analysis
|
|
256
|
+
if (this.micEngine) {
|
|
257
|
+
this.micEngine.disconnectMusicSource(this.audioSource);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
this.audioSource.stop();
|
|
262
|
+
} catch {
|
|
263
|
+
// Already stopped
|
|
264
|
+
}
|
|
265
|
+
this.audioSource = null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.stopRendering();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
seek(positionSec) {
|
|
272
|
+
const wasPlaying = this.isPlaying;
|
|
273
|
+
|
|
274
|
+
// Pause if playing
|
|
275
|
+
if (wasPlaying) {
|
|
276
|
+
this.pause();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Set new position
|
|
280
|
+
this.pauseTime = positionSec;
|
|
281
|
+
this.currentTime = positionSec;
|
|
282
|
+
|
|
283
|
+
// Resume if it was playing
|
|
284
|
+
if (wasPlaying) {
|
|
285
|
+
this.play();
|
|
286
|
+
} else {
|
|
287
|
+
// Force a frame render at new position
|
|
288
|
+
this.renderFrame();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
startRendering() {
|
|
293
|
+
if (this.animationFrame) return;
|
|
294
|
+
|
|
295
|
+
const render = () => {
|
|
296
|
+
if (!this.isPlaying) return;
|
|
297
|
+
|
|
298
|
+
this.renderFrame();
|
|
299
|
+
this.animationFrame = requestAnimationFrame(render);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
render();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
stopRendering() {
|
|
306
|
+
if (this.animationFrame) {
|
|
307
|
+
cancelAnimationFrame(this.animationFrame);
|
|
308
|
+
this.animationFrame = null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
renderFrame() {
|
|
313
|
+
if (!this.cdgPlayer) return;
|
|
314
|
+
|
|
315
|
+
// Get current time from Web Audio API
|
|
316
|
+
this.currentTime = this.getCurrentTime();
|
|
317
|
+
|
|
318
|
+
// Get CDG frame for current time
|
|
319
|
+
const result = this.cdgPlayer.render(this.currentTime);
|
|
320
|
+
|
|
321
|
+
if (!result || !result.imageData) {
|
|
322
|
+
console.warn('💿 No frame data at time:', this.currentTime);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Make CDG background transparent for Butterchurn to show through
|
|
327
|
+
const imageData = result.imageData;
|
|
328
|
+
const data = imageData.data;
|
|
329
|
+
|
|
330
|
+
// Get the background color from the CDG result (typically index 0 in palette)
|
|
331
|
+
// The backgroundColor is usually at position 0,0
|
|
332
|
+
const bgR = data[0];
|
|
333
|
+
const bgG = data[1];
|
|
334
|
+
const bgB = data[2];
|
|
335
|
+
|
|
336
|
+
// Store background color for overlay
|
|
337
|
+
this.cdgBackgroundColor = { r: bgR, g: bgG, b: bgB };
|
|
338
|
+
|
|
339
|
+
// Make all pixels matching the background color transparent
|
|
340
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
341
|
+
const r = data[i];
|
|
342
|
+
const g = data[i + 1];
|
|
343
|
+
const b = data[i + 2];
|
|
344
|
+
|
|
345
|
+
// If pixel matches background color, make it transparent
|
|
346
|
+
if (r === bgR && g === bgG && b === bgB) {
|
|
347
|
+
data[i + 3] = 0; // Set alpha to 0 (transparent)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Convert modified ImageData to canvas
|
|
352
|
+
this.cdgCtx.putImageData(imageData, 0, 0);
|
|
353
|
+
|
|
354
|
+
// Clear main canvas
|
|
355
|
+
this.ctx.fillStyle = '#000';
|
|
356
|
+
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
357
|
+
|
|
358
|
+
// Draw Butterchurn background effects if enabled
|
|
359
|
+
if (this.effectsEnabled && this.effectsCanvas && this.butterchurn) {
|
|
360
|
+
try {
|
|
361
|
+
this.butterchurn.render();
|
|
362
|
+
this.ctx.drawImage(this.effectsCanvas, 0, 0, this.canvas.width, this.canvas.height);
|
|
363
|
+
} catch {
|
|
364
|
+
// Effects rendering can fail, don't crash the whole renderer
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Draw CDG background color as semi-transparent overlay (like KAI renderer does)
|
|
369
|
+
// This respects the overlayOpacity setting for consistent look
|
|
370
|
+
const overlayOpacity = this.overlayOpacity || 0.7;
|
|
371
|
+
this.ctx.save();
|
|
372
|
+
this.ctx.globalAlpha = overlayOpacity;
|
|
373
|
+
this.ctx.fillStyle = `rgb(${bgR}, ${bgG}, ${bgB})`;
|
|
374
|
+
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
375
|
+
this.ctx.restore();
|
|
376
|
+
|
|
377
|
+
// Scale and center CDG graphics on top
|
|
378
|
+
// CDG is 300x216, scale 5x to 1500x1080 for 1080p
|
|
379
|
+
const scale = 5;
|
|
380
|
+
const cdgWidth = 300 * scale; // 1500
|
|
381
|
+
const cdgHeight = 216 * scale; // 1080
|
|
382
|
+
|
|
383
|
+
// Center horizontally in 1920px canvas (210px margins on each side)
|
|
384
|
+
const offsetX = (this.canvas.width - cdgWidth) / 2;
|
|
385
|
+
const offsetY = 0; // Fill height
|
|
386
|
+
|
|
387
|
+
// Draw scaled CDG graphics on top (text and graphics only, no background)
|
|
388
|
+
this.ctx.imageSmoothingEnabled = false; // Pixel-perfect scaling
|
|
389
|
+
this.ctx.drawImage(this.cdgCanvas, offsetX, offsetY, cdgWidth, cdgHeight);
|
|
390
|
+
|
|
391
|
+
// Draw QR code overlay if enabled
|
|
392
|
+
this.drawQRCodeOverlay();
|
|
393
|
+
|
|
394
|
+
// Draw queue display if enabled
|
|
395
|
+
this.drawQueueDisplay();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Draw QR code in bottom left corner (only when not playing)
|
|
400
|
+
*/
|
|
401
|
+
drawQRCodeOverlay() {
|
|
402
|
+
// Only show when not playing
|
|
403
|
+
if (!this.showQrCode || !this.qrCodeCanvas || this.isPlaying) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const padding = 20;
|
|
408
|
+
const qrSize = 150;
|
|
409
|
+
const x = padding; // Bottom left instead of right
|
|
410
|
+
const y = this.canvas.height - qrSize - padding;
|
|
411
|
+
|
|
412
|
+
// Draw white background with shadow
|
|
413
|
+
this.ctx.save();
|
|
414
|
+
this.ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
|
|
415
|
+
this.ctx.shadowBlur = 10;
|
|
416
|
+
this.ctx.shadowOffsetX = 2;
|
|
417
|
+
this.ctx.shadowOffsetY = 2;
|
|
418
|
+
this.ctx.fillStyle = '#FFFFFF';
|
|
419
|
+
this.ctx.fillRect(x - 10, y - 10, qrSize + 20, qrSize + 20);
|
|
420
|
+
this.ctx.restore();
|
|
421
|
+
|
|
422
|
+
// Draw QR code
|
|
423
|
+
this.ctx.drawImage(this.qrCodeCanvas, x, y, qrSize, qrSize);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Draw queue display in bottom right corner (only when not playing)
|
|
428
|
+
*/
|
|
429
|
+
drawQueueDisplay() {
|
|
430
|
+
// Only show when setting is enabled and queue has items
|
|
431
|
+
if (!this.displayQueue || !this.queueItems || this.queueItems.length === 0 || this.isPlaying) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const width = this.canvas.width;
|
|
436
|
+
const height = this.canvas.height;
|
|
437
|
+
const padding = 120; // Move further from edge (left)
|
|
438
|
+
const bottomPadding = 80; // Move up from bottom
|
|
439
|
+
const rightX = width - padding;
|
|
440
|
+
const lineHeight = 64;
|
|
441
|
+
const labelFontSize = 48;
|
|
442
|
+
const songFontSize = 40;
|
|
443
|
+
|
|
444
|
+
this.ctx.save();
|
|
445
|
+
|
|
446
|
+
// Calculate text dimensions for background
|
|
447
|
+
this.ctx.font = `bold ${labelFontSize}px sans-serif`;
|
|
448
|
+
const labelText = 'Next up:';
|
|
449
|
+
const labelWidth = this.ctx.measureText(labelText).width;
|
|
450
|
+
|
|
451
|
+
// Measure all song texts and prepare data
|
|
452
|
+
let maxWidth = labelWidth;
|
|
453
|
+
const songData = this.queueItems.slice(0, 3).map((item) => {
|
|
454
|
+
const title = item.title || item.song?.title || 'Unknown';
|
|
455
|
+
const singer = item.requester || item.singer || '';
|
|
456
|
+
|
|
457
|
+
// Measure title
|
|
458
|
+
this.ctx.font = `${songFontSize}px sans-serif`;
|
|
459
|
+
const titleWidth = this.ctx.measureText(title).width;
|
|
460
|
+
|
|
461
|
+
// Measure singer if present
|
|
462
|
+
let singerWidth = 0;
|
|
463
|
+
if (singer) {
|
|
464
|
+
const singerText = ` - ${singer}`;
|
|
465
|
+
singerWidth = this.ctx.measureText(singerText).width;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const totalWidth = titleWidth + singerWidth;
|
|
469
|
+
maxWidth = Math.max(maxWidth, totalWidth);
|
|
470
|
+
|
|
471
|
+
return { title, singer };
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Calculate background dimensions
|
|
475
|
+
const bgWidth = maxWidth + 30;
|
|
476
|
+
const bgHeight = lineHeight + songData.length * lineHeight + 20;
|
|
477
|
+
const bgX = rightX - bgWidth;
|
|
478
|
+
const bgY = height - bgHeight - bottomPadding;
|
|
479
|
+
|
|
480
|
+
// Draw semi-transparent background with shadow and rounded corners
|
|
481
|
+
this.ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
|
|
482
|
+
this.ctx.shadowBlur = 10;
|
|
483
|
+
this.ctx.shadowOffsetX = 2;
|
|
484
|
+
this.ctx.shadowOffsetY = 2;
|
|
485
|
+
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
|
486
|
+
|
|
487
|
+
// Draw rounded rectangle
|
|
488
|
+
const radius = 10;
|
|
489
|
+
this.ctx.beginPath();
|
|
490
|
+
this.ctx.moveTo(bgX + radius, bgY);
|
|
491
|
+
this.ctx.lineTo(bgX + bgWidth - radius, bgY);
|
|
492
|
+
this.ctx.quadraticCurveTo(bgX + bgWidth, bgY, bgX + bgWidth, bgY + radius);
|
|
493
|
+
this.ctx.lineTo(bgX + bgWidth, bgY + bgHeight - radius);
|
|
494
|
+
this.ctx.quadraticCurveTo(
|
|
495
|
+
bgX + bgWidth,
|
|
496
|
+
bgY + bgHeight,
|
|
497
|
+
bgX + bgWidth - radius,
|
|
498
|
+
bgY + bgHeight
|
|
499
|
+
);
|
|
500
|
+
this.ctx.lineTo(bgX + radius, bgY + bgHeight);
|
|
501
|
+
this.ctx.quadraticCurveTo(bgX, bgY + bgHeight, bgX, bgY + bgHeight - radius);
|
|
502
|
+
this.ctx.lineTo(bgX, bgY + radius);
|
|
503
|
+
this.ctx.quadraticCurveTo(bgX, bgY, bgX + radius, bgY);
|
|
504
|
+
this.ctx.closePath();
|
|
505
|
+
this.ctx.fill();
|
|
506
|
+
|
|
507
|
+
this.ctx.shadowColor = 'transparent';
|
|
508
|
+
|
|
509
|
+
// Draw "Next up:" label in blue
|
|
510
|
+
this.ctx.font = `bold ${labelFontSize}px sans-serif`;
|
|
511
|
+
this.ctx.fillStyle = '#3B82F6'; // Tailwind blue-600
|
|
512
|
+
this.ctx.textAlign = 'left';
|
|
513
|
+
this.ctx.fillText(labelText, bgX + 15, bgY + labelFontSize + 10);
|
|
514
|
+
|
|
515
|
+
// Draw queue items
|
|
516
|
+
this.ctx.font = `${songFontSize}px sans-serif`;
|
|
517
|
+
songData.forEach((item, index) => {
|
|
518
|
+
const textY = bgY + labelFontSize + 10 + (index + 1) * lineHeight;
|
|
519
|
+
const textX = bgX + 15;
|
|
520
|
+
|
|
521
|
+
// Draw title in white
|
|
522
|
+
this.ctx.fillStyle = '#FFFFFF';
|
|
523
|
+
this.ctx.fillText(item.title, textX, textY);
|
|
524
|
+
|
|
525
|
+
// Draw singer in yellow if present and not "KJ"
|
|
526
|
+
if (item.singer) {
|
|
527
|
+
const titleWidth = this.ctx.measureText(item.title).width;
|
|
528
|
+
const isKJ = item.singer.toUpperCase() === 'KJ';
|
|
529
|
+
this.ctx.fillStyle = isKJ ? '#FFFFFF' : '#FCD34D'; // yellow-300 for non-KJ singers
|
|
530
|
+
this.ctx.fillText(` - ${item.singer}`, textX + titleWidth, textY);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
this.ctx.restore();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
handleSongEnd() {
|
|
538
|
+
this.stopRendering();
|
|
539
|
+
|
|
540
|
+
// Use base class method for consistent song end handling
|
|
541
|
+
this._triggerSongEnd();
|
|
542
|
+
|
|
543
|
+
// Notify main process (for backward compatibility)
|
|
544
|
+
if (window.electronAPI && window.electronAPI.queue) {
|
|
545
|
+
window.electronAPI.queue.notifyComplete();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
setEffectsCanvas(canvas, butterchurn) {
|
|
550
|
+
this.effectsCanvas = canvas;
|
|
551
|
+
this.butterchurn = butterchurn;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
setEffectsEnabled(enabled) {
|
|
555
|
+
this.effectsEnabled = enabled;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
getCurrentTime() {
|
|
559
|
+
if (this.isPlaying && this.audioContext) {
|
|
560
|
+
return this.audioContext.currentTime - this.startTime;
|
|
561
|
+
}
|
|
562
|
+
return this.pauseTime || 0;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Implements PlayerInterface method - alias for getCurrentTime()
|
|
567
|
+
* @returns {number} Current position in seconds
|
|
568
|
+
*/
|
|
569
|
+
getCurrentPosition() {
|
|
570
|
+
return this.getCurrentTime();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
getDuration() {
|
|
574
|
+
return this.audioBuffer ? this.audioBuffer.duration : 0;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Note: reportStateChange(), startStateReporting(), and stopStateReporting()
|
|
579
|
+
* are inherited from PlayerInterface base class
|
|
580
|
+
*/
|
|
581
|
+
|
|
582
|
+
async setAudioContext(audioContext, gainNode, analyserNode) {
|
|
583
|
+
this.audioContext = audioContext;
|
|
584
|
+
this.gainNode = gainNode;
|
|
585
|
+
this.analyserNode = analyserNode;
|
|
586
|
+
|
|
587
|
+
// Initialize microphone engine with PA context
|
|
588
|
+
this.micEngine = new MicrophoneEngine(audioContext, gainNode, {
|
|
589
|
+
getCurrentPosition: () => this.getCurrentPosition(),
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Load auto-tune worklets
|
|
593
|
+
await this.micEngine.loadAutoTuneWorklet();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async loadAutoTuneWorklet() {
|
|
597
|
+
if (this.micEngine) {
|
|
598
|
+
await this.micEngine.loadAutoTuneWorklet();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async startMicrophoneInput(deviceId = 'default') {
|
|
603
|
+
if (this.micEngine) {
|
|
604
|
+
await this.micEngine.startMicrophoneInput(deviceId);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
enableAutoTune() {
|
|
609
|
+
if (this.micEngine) {
|
|
610
|
+
this.micEngine.enableAutoTune();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
disableAutoTune() {
|
|
615
|
+
if (this.micEngine) {
|
|
616
|
+
this.micEngine.disableAutoTune();
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
setAutoTuneSettings(settings) {
|
|
621
|
+
if (this.micEngine) {
|
|
622
|
+
this.micEngine.setAutoTuneSettings(settings);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
stopMicrophoneInput() {
|
|
627
|
+
if (this.micEngine) {
|
|
628
|
+
this.micEngine.stopMicrophoneInput();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
setMicToSpeakers(enabled) {
|
|
633
|
+
if (this.micEngine) {
|
|
634
|
+
this.micEngine.setMicToSpeakers(enabled);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async setEnableMic(enabled) {
|
|
639
|
+
if (this.micEngine) {
|
|
640
|
+
await this.micEngine.setEnableMic(enabled);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
setMicrophoneGain(gainValue) {
|
|
645
|
+
if (this.micEngine) {
|
|
646
|
+
this.micEngine.setMicrophoneGain(gainValue);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
destroy() {
|
|
651
|
+
super.destroy(); // Call parent cleanup (stops state reporting)
|
|
652
|
+
|
|
653
|
+
// Stop microphone engine
|
|
654
|
+
if (this.micEngine) {
|
|
655
|
+
this.micEngine.stopMicrophoneInput();
|
|
656
|
+
this.micEngine = null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
this.stopRendering();
|
|
660
|
+
if (this.audioSource) {
|
|
661
|
+
// Disconnect from music analysis
|
|
662
|
+
if (this.micEngine) {
|
|
663
|
+
this.micEngine.disconnectMusicSource(this.audioSource);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
try {
|
|
667
|
+
this.audioSource.stop();
|
|
668
|
+
} catch {
|
|
669
|
+
// Already stopped
|
|
670
|
+
}
|
|
671
|
+
this.audioSource = null;
|
|
672
|
+
}
|
|
673
|
+
this.audioBuffer = null;
|
|
674
|
+
this.cdgPlayer = null;
|
|
675
|
+
this.cdgData = null;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Get the format type this player handles
|
|
680
|
+
* @returns {string} Format name
|
|
681
|
+
*/
|
|
682
|
+
getFormat() {
|
|
683
|
+
return 'cdg';
|
|
684
|
+
}
|
|
685
|
+
}
|