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.
Files changed (290) hide show
  1. package/README.md +558 -0
  2. package/bin/loukai.js +32 -0
  3. package/package.json +243 -0
  4. package/src/main/appState.js +250 -0
  5. package/src/main/audioEngine.js +478 -0
  6. package/src/main/creator/conversionService.js +503 -0
  7. package/src/main/creator/downloadManager.js +1128 -0
  8. package/src/main/creator/ffmpegService.js +487 -0
  9. package/src/main/creator/installLogger.js +51 -0
  10. package/src/main/creator/keyDetection.js +212 -0
  11. package/src/main/creator/llmService.js +370 -0
  12. package/src/main/creator/lrclibService.js +340 -0
  13. package/src/main/creator/python/crepe_runner.py +189 -0
  14. package/src/main/creator/python/demucs_runner.py +158 -0
  15. package/src/main/creator/python/whisper_runner.py +172 -0
  16. package/src/main/creator/pythonRunner.js +268 -0
  17. package/src/main/creator/stemBuilder.js +491 -0
  18. package/src/main/creator/systemChecker.js +474 -0
  19. package/src/main/handlers/appHandlers.js +45 -0
  20. package/src/main/handlers/audioHandlers.js +33 -0
  21. package/src/main/handlers/autotuneHandlers.js +28 -0
  22. package/src/main/handlers/canvasHandlers.js +84 -0
  23. package/src/main/handlers/creatorHandlers.js +159 -0
  24. package/src/main/handlers/editorHandlers.js +98 -0
  25. package/src/main/handlers/effectsHandlers.js +100 -0
  26. package/src/main/handlers/fileHandlers.js +45 -0
  27. package/src/main/handlers/index.js +78 -0
  28. package/src/main/handlers/libraryHandlers.js +96 -0
  29. package/src/main/handlers/mixerHandlers.js +64 -0
  30. package/src/main/handlers/playerHandlers.js +39 -0
  31. package/src/main/handlers/preferencesHandlers.js +46 -0
  32. package/src/main/handlers/queueHandlers.js +81 -0
  33. package/src/main/handlers/rendererHandlers.js +63 -0
  34. package/src/main/handlers/settingsHandlers.js +42 -0
  35. package/src/main/handlers/webServerHandlers.js +105 -0
  36. package/src/main/main.js +2351 -0
  37. package/src/main/preload.js +252 -0
  38. package/src/main/settingsManager.js +139 -0
  39. package/src/main/statePersistence.js +193 -0
  40. package/src/main/utils/pathValidator.js +112 -0
  41. package/src/main/webServer.js +2535 -0
  42. package/src/native/autotune.js +417 -0
  43. package/src/renderer/adapters/ElectronBridge.js +677 -0
  44. package/src/renderer/canvas.html +80 -0
  45. package/src/renderer/components/App.jsx +303 -0
  46. package/src/renderer/components/AppRoot.jsx +37 -0
  47. package/src/renderer/components/AudioDeviceSettings.jsx +145 -0
  48. package/src/renderer/components/EffectsPanelWrapper.jsx +267 -0
  49. package/src/renderer/components/MixerTab.jsx +233 -0
  50. package/src/renderer/components/MixerTabWrapper.jsx +31 -0
  51. package/src/renderer/components/PortalSelect.jsx +239 -0
  52. package/src/renderer/components/QueueTab.jsx +116 -0
  53. package/src/renderer/components/RequestsListWrapper.jsx +78 -0
  54. package/src/renderer/components/ServerTab.jsx +472 -0
  55. package/src/renderer/components/SongInfoBarWrapper.jsx +77 -0
  56. package/src/renderer/components/StatusBar.jsx +92 -0
  57. package/src/renderer/components/TabNavigation.jsx +77 -0
  58. package/src/renderer/components/TransportControlsWrapper.jsx +69 -0
  59. package/src/renderer/components/creator/CreateTab.jsx +1236 -0
  60. package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js +2 -0
  61. package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js.map +1 -0
  62. package/src/renderer/dist/assets/microphoneEngine-BaCUhhQc.js +2 -0
  63. package/src/renderer/dist/assets/microphoneEngine-BaCUhhQc.js.map +1 -0
  64. package/src/renderer/dist/assets/player-DVrqp7N5.js +3 -0
  65. package/src/renderer/dist/assets/player-DVrqp7N5.js.map +1 -0
  66. package/src/renderer/dist/assets/songLoaders-BaTgGib4.js +2 -0
  67. package/src/renderer/dist/assets/songLoaders-BaTgGib4.js.map +1 -0
  68. package/src/renderer/dist/assets/webrtcManager-BhCHWceK.js +2 -0
  69. package/src/renderer/dist/assets/webrtcManager-BhCHWceK.js.map +1 -0
  70. package/src/renderer/dist/js/autoTuneWorklet.js +224 -0
  71. package/src/renderer/dist/js/micPitchDetectorWorklet.js +137 -0
  72. package/src/renderer/dist/js/musicAnalysisWorklet.js +216 -0
  73. package/src/renderer/dist/js/phaseVocoderWorklet.js +341 -0
  74. package/src/renderer/dist/js/soundtouch-worklet.js +1395 -0
  75. package/src/renderer/dist/renderer.css +1 -0
  76. package/src/renderer/dist/renderer.js +62 -0
  77. package/src/renderer/dist/renderer.js.map +1 -0
  78. package/src/renderer/dist/renderer.woff2 +0 -0
  79. package/src/renderer/hooks/useKeyboardShortcuts.js +154 -0
  80. package/src/renderer/index.html +24 -0
  81. package/src/renderer/index.html.backup +372 -0
  82. package/src/renderer/js/PlayerInterface.js +267 -0
  83. package/src/renderer/js/autoTuneWorklet.js +224 -0
  84. package/src/renderer/js/butterchurnVerify.js +46 -0
  85. package/src/renderer/js/canvas-app.js +114 -0
  86. package/src/renderer/js/cdgPlayer.js +685 -0
  87. package/src/renderer/js/kaiPlayer.js +1200 -0
  88. package/src/renderer/js/karaokeRenderer.js +3392 -0
  89. package/src/renderer/js/micPitchDetectorWorklet.js +137 -0
  90. package/src/renderer/js/microphoneEngine.js +656 -0
  91. package/src/renderer/js/musicAnalysisWorklet.js +216 -0
  92. package/src/renderer/js/phaseVocoderWorklet.js +341 -0
  93. package/src/renderer/js/player.js +232 -0
  94. package/src/renderer/js/referencePitchTracker.js +130 -0
  95. package/src/renderer/js/songLoaders.js +334 -0
  96. package/src/renderer/js/soundtouch-worklet.js +1395 -0
  97. package/src/renderer/js/webrtcManager.js +511 -0
  98. package/src/renderer/lib/butterchurn.min.js +6739 -0
  99. package/src/renderer/lib/butterchurnPresets.min.js +1 -0
  100. package/src/renderer/lib/cdgraphics-wrapper.js +16 -0
  101. package/src/renderer/lib/cdgraphics.js +299 -0
  102. package/src/renderer/public/js/autoTuneWorklet.js +224 -0
  103. package/src/renderer/public/js/micPitchDetectorWorklet.js +137 -0
  104. package/src/renderer/public/js/musicAnalysisWorklet.js +216 -0
  105. package/src/renderer/public/js/phaseVocoderWorklet.js +341 -0
  106. package/src/renderer/public/js/soundtouch-worklet.js +1395 -0
  107. package/src/renderer/react-entry.jsx +44 -0
  108. package/src/renderer/styles/tailwind.css +106 -0
  109. package/src/renderer/utils/qrCodeGenerator.js +98 -0
  110. package/src/renderer/vite.config.js +31 -0
  111. package/src/shared/adapters/BridgeInterface.js +195 -0
  112. package/src/shared/components/EffectsPanel.jsx +177 -0
  113. package/src/shared/components/LibraryPanel.jsx +701 -0
  114. package/src/shared/components/LineDetailCanvas.jsx +167 -0
  115. package/src/shared/components/LyricLine.jsx +505 -0
  116. package/src/shared/components/LyricRejection.jsx +84 -0
  117. package/src/shared/components/LyricSuggestion.jsx +80 -0
  118. package/src/shared/components/LyricsEditorCanvas.jsx +271 -0
  119. package/src/shared/components/MixerPanel.jsx +94 -0
  120. package/src/shared/components/PlayerControls.jsx +206 -0
  121. package/src/shared/components/PortalSelect.jsx +239 -0
  122. package/src/shared/components/QueueList.jsx +365 -0
  123. package/src/shared/components/QuickSearch.jsx +126 -0
  124. package/src/shared/components/RequestsList.jsx +121 -0
  125. package/src/shared/components/SongEditor.jsx +1362 -0
  126. package/src/shared/components/SongInfoBar.jsx +81 -0
  127. package/src/shared/components/ThemeToggle.jsx +106 -0
  128. package/src/shared/components/Toast.jsx +30 -0
  129. package/src/shared/components/VisualizationSettings.jsx +243 -0
  130. package/src/shared/constants.js +95 -0
  131. package/src/shared/context/BridgeContext.jsx +32 -0
  132. package/src/shared/contexts/AudioContext.jsx +37 -0
  133. package/src/shared/contexts/PlayerContext.jsx +66 -0
  134. package/src/shared/contexts/SettingsContext.jsx +50 -0
  135. package/src/shared/defaults.js +158 -0
  136. package/src/shared/formatUtils.js +59 -0
  137. package/src/shared/formatUtils.test.js +207 -0
  138. package/src/shared/hooks/useAppState.js +97 -0
  139. package/src/shared/hooks/useAudioEngine.js +264 -0
  140. package/src/shared/hooks/usePlayer.js +89 -0
  141. package/src/shared/hooks/useSettingsPersistence.js +74 -0
  142. package/src/shared/hooks/useWebRTC.js +118 -0
  143. package/src/shared/ipcContracts.js +299 -0
  144. package/src/shared/package.json +3 -0
  145. package/src/shared/services/creatorService.js +373 -0
  146. package/src/shared/services/creatorService.test.js +413 -0
  147. package/src/shared/services/editorService.js +213 -0
  148. package/src/shared/services/editorService.test.js +219 -0
  149. package/src/shared/services/effectsService.js +271 -0
  150. package/src/shared/services/effectsService.test.js +418 -0
  151. package/src/shared/services/libraryService.js +438 -0
  152. package/src/shared/services/libraryService.test.js +474 -0
  153. package/src/shared/services/mixerService.js +172 -0
  154. package/src/shared/services/mixerService.test.js +399 -0
  155. package/src/shared/services/playerService.js +221 -0
  156. package/src/shared/services/playerService.test.js +357 -0
  157. package/src/shared/services/preferencesService.js +219 -0
  158. package/src/shared/services/queueService.js +226 -0
  159. package/src/shared/services/queueService.test.js +430 -0
  160. package/src/shared/services/requestsService.js +155 -0
  161. package/src/shared/services/requestsService.test.js +362 -0
  162. package/src/shared/services/serverSettingsService.js +151 -0
  163. package/src/shared/services/settingsService.js +257 -0
  164. package/src/shared/services/settingsService.test.js +295 -0
  165. package/src/shared/state/StateManager.js +263 -0
  166. package/src/shared/utils/audio.js +42 -0
  167. package/src/shared/utils/format.js +32 -0
  168. package/src/shared/utils/lyricsUtils.js +162 -0
  169. package/src/test/setup.js +40 -0
  170. package/src/utils/cdgLoader.js +180 -0
  171. package/src/utils/m4aLoader.js +333 -0
  172. package/src/web/App.jsx +578 -0
  173. package/src/web/adapters/WebBridge.js +428 -0
  174. package/src/web/components/PlayerSettingsPanel.jsx +231 -0
  175. package/src/web/components/SongSearch.jsx +180 -0
  176. package/src/web/dist/assets/index-0H-RnRrV.js +51 -0
  177. package/src/web/dist/assets/index-0H-RnRrV.js.map +1 -0
  178. package/src/web/dist/assets/index-DYW2zB0u.css +1 -0
  179. package/src/web/dist/index.html +15 -0
  180. package/src/web/index.html +14 -0
  181. package/src/web/main.jsx +10 -0
  182. package/src/web/package-lock.json +1765 -0
  183. package/src/web/pages/SongRequestPage.jsx +619 -0
  184. package/src/web/styles/tailwind.css +68 -0
  185. package/src/web/vite.config.js +27 -0
  186. package/static/fonts/material-icons.woff2 +0 -0
  187. package/static/images/butterchurn-screenshots/Aderrasi - Potion of Spirits.png +0 -0
  188. package/static/images/butterchurn-screenshots/Aderrasi - Songflower _Moss Posy_.png +0 -0
  189. package/static/images/butterchurn-screenshots/Aderrasi - Storm of the Eye _Thunder_ - mash0000 - quasi pseudo meta concentrics.png +0 -0
  190. package/static/images/butterchurn-screenshots/Aderrasi _ Geiss - Airhandler _Kali Mix_ - Canvas Mix.png +0 -0
  191. 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
  192. package/static/images/butterchurn-screenshots/Cope - The Neverending Explosion of Red Liquid Fire.png +0 -0
  193. proton lights __Krash_s beat code_ _Phat_remix02b.png +0 -0
  194. package/static/images/butterchurn-screenshots/Eo_S_ _ Phat - cubetrace - v2.png +0 -0
  195. package/static/images/butterchurn-screenshots/Eo_S_ _ Zylot - skylight _Stained Glass Majesty mix_.png +0 -0
  196. package/static/images/butterchurn-screenshots/Flexi - alien fish pond.png +0 -0
  197. package/static/images/butterchurn-screenshots/Flexi - area 51.png +0 -0
  198. package/static/images/butterchurn-screenshots/Flexi - infused with the spiral.png +0 -0
  199. package/static/images/butterchurn-screenshots/Flexi - mindblob _shiny mix_.png +0 -0
  200. package/static/images/butterchurn-screenshots/Flexi - mindblob mix.png +0 -0
  201. package/static/images/butterchurn-screenshots/Flexi - predator-prey-spirals.png +0 -0
  202. package/static/images/butterchurn-screenshots/Flexi - smashing fractals _acid etching mix_.png +0 -0
  203. package/static/images/butterchurn-screenshots/Flexi - truly soft piece of software - this is generic texturing _Jelly_ .png +0 -0
  204. package/static/images/butterchurn-screenshots/Flexi _ Martin - astral projection.png +0 -0
  205. package/static/images/butterchurn-screenshots/Flexi _ Martin - cascading decay swing.png +0 -0
  206. package/static/images/butterchurn-screenshots/Flexi _ amandio c - piercing 05 - Kopie _2_ - Kopie.png +0 -0
  207. package/static/images/butterchurn-screenshots/Flexi _ stahlregen - jelly showoff parade.png +0 -0
  208. package/static/images/butterchurn-screenshots/Flexi_ fishbrain_ Geiss _ Martin - tokamak witchery.png +0 -0
  209. package/static/images/butterchurn-screenshots/Flexi_ martin _ geiss - dedicated to the sherwin maxawow.png +0 -0
  210. package/static/images/butterchurn-screenshots/Fumbling_Foo _ Flexi_ Martin_ Orb_ Unchained - Star Nova v7b.png +0 -0
  211. package/static/images/butterchurn-screenshots/Geiss - Cauldron - painterly 2 _saturation remix_.png +0 -0
  212. package/static/images/butterchurn-screenshots/Geiss - Reaction Diffusion 2.png +0 -0
  213. package/static/images/butterchurn-screenshots/Geiss - Spiral Artifact.png +0 -0
  214. package/static/images/butterchurn-screenshots/Geiss - Thumb Drum.png +0 -0
  215. package/static/images/butterchurn-screenshots/Geiss _ Flexi _ Martin - disconnected.png +0 -0
  216. package/static/images/butterchurn-screenshots/Geiss_ Flexi _ Stahlregen - Thumbdrum Tokamak _crossfiring aftermath jelly mashup_.png +0 -0
  217. package/static/images/butterchurn-screenshots/Goody - The Wild Vort.png +0 -0
  218. package/static/images/butterchurn-screenshots/Idiot - Star Of Annon.png +0 -0
  219. package/static/images/butterchurn-screenshots/Krash _ Illusion - Spiral Movement.png +0 -0
  220. package/static/images/butterchurn-screenshots/Martin - QBikal - Surface Turbulence IIb.png +0 -0
  221. package/static/images/butterchurn-screenshots/Martin - acid wiring.png +0 -0
  222. package/static/images/butterchurn-screenshots/Martin - charisma.png +0 -0
  223. package/static/images/butterchurn-screenshots/Martin - liquid arrows.png +0 -0
  224. package/static/images/butterchurn-screenshots/Milk Artist At our Best - FED - SlowFast Ft AdamFX n Martin - HD CosmoFX.png +0 -0
  225. package/static/images/butterchurn-screenshots/ORB - Waaa.png +0 -0
  226. package/static/images/butterchurn-screenshots/Phat_fiShbRaiN_Eo_S_Mandala_Chasers_remix.png +0 -0
  227. package/static/images/butterchurn-screenshots/Rovastar - Oozing Resistance.png +0 -0
  228. package/static/images/butterchurn-screenshots/Rovastar _ Loadus _ Geiss - FractalDrop _Triple Mix_.png +0 -0
  229. package/static/images/butterchurn-screenshots/TonyMilkdrop - Leonardo Da Vinci_s Balloon _Flexi - merry-go-round _ techstyle_.png +0 -0
  230. package/static/images/butterchurn-screenshots/TonyMilkdrop - Magellan_s Nebula _Flexi - you enter first _ multiverse_.png +0 -0
  231. package/static/images/butterchurn-screenshots/Unchained - Rewop.png +0 -0
  232. package/static/images/butterchurn-screenshots/Unchained - Unified Drag 2.png +0 -0
  233. package/static/images/butterchurn-screenshots/Unchained _ Rovastar - Wormhole Pillars _Hall of Shadows mix_.png +0 -0
  234. package/static/images/butterchurn-screenshots/Zylot - Paint Spill _Music Reactive Paint Mix_.png +0 -0
  235. package/static/images/butterchurn-screenshots/Zylot - Star Ornament.png +0 -0
  236. package/static/images/butterchurn-screenshots/Zylot - True Visionary _Final Mix_.png +0 -0
  237. package/static/images/butterchurn-screenshots/_Aderrasi - Wanderer in Curved Space - mash0000 - faclempt kibitzing meshuggana schmaltz _Geiss color mix_.png +0 -0
  238. package/static/images/butterchurn-screenshots/_Geiss - Artifact 01.png +0 -0
  239. package/static/images/butterchurn-screenshots/_Geiss - Desert Rose 2.png +0 -0
  240. package/static/images/butterchurn-screenshots/_Geiss - untitled.png +0 -0
  241. package/static/images/butterchurn-screenshots/_Mig_049.png +0 -0
  242. package/static/images/butterchurn-screenshots/_Mig_085.png +0 -0
  243. package/static/images/butterchurn-screenshots/_Rovastar _ Geiss - Hurricane Nightmare _Posterize Mix_.png +0 -0
  244. package/static/images/butterchurn-screenshots/___ Royal - Mashup _197_.png +0 -0
  245. package/static/images/butterchurn-screenshots/___ Royal - Mashup _220_.png +0 -0
  246. package/static/images/butterchurn-screenshots/___ Royal - Mashup _431_.png +0 -0
  247. package/static/images/butterchurn-screenshots/cope _ martin - mother-of-pearl.png +0 -0
  248. package/static/images/butterchurn-screenshots/fiShbRaiN _ Flexi - witchcraft 2_0.png +0 -0
  249. package/static/images/butterchurn-screenshots/flexi - bouncing balls _double mindblob neon mix_.png +0 -0
  250. package/static/images/butterchurn-screenshots/flexi - mom_ why the sky looks different today.png +0 -0
  251. package/static/images/butterchurn-screenshots/flexi - patternton_ district of media_ capitol of the united abstractions of fractopia.png +0 -0
  252. package/static/images/butterchurn-screenshots/flexi - swing out on the spiral.png +0 -0
  253. package/static/images/butterchurn-screenshots/flexi - what is the matrix.png +0 -0
  254. package/static/images/butterchurn-screenshots/flexi _ amandio c - organic _random mashup_.png +0 -0
  255. package/static/images/butterchurn-screenshots/flexi _ amandio c - organic12-3d-2_milk.png +0 -0
  256. package/static/images/butterchurn-screenshots/flexi _ fishbrain - neon mindblob grafitti.png +0 -0
  257. package/static/images/butterchurn-screenshots/flexi _ geiss - pogo cubes vs_ tokamak vs_ game of life _stahls jelly 4_5 finish_.png +0 -0
  258. package/static/images/butterchurn-screenshots/high-altitude basket unraveling - singh grooves nitrogen argon nz_.png +0 -0
  259. package/static/images/butterchurn-screenshots/martin - The Bridge of Khazad-Dum.png +0 -0
  260. package/static/images/butterchurn-screenshots/martin - angel flight.png +0 -0
  261. package/static/images/butterchurn-screenshots/martin - another kind of groove.png +0 -0
  262. package/static/images/butterchurn-screenshots/martin - bombyx mori.png +0 -0
  263. package/static/images/butterchurn-screenshots/martin - castle in the air.png +0 -0
  264. package/static/images/butterchurn-screenshots/martin - chain breaker.png +0 -0
  265. package/static/images/butterchurn-screenshots/martin - disco mix 4.png +0 -0
  266. package/static/images/butterchurn-screenshots/martin - extreme heat.png +0 -0
  267. package/static/images/butterchurn-screenshots/martin - frosty caves 2.png +0 -0
  268. package/static/images/butterchurn-screenshots/martin - fruit machine.png +0 -0
  269. package/static/images/butterchurn-screenshots/martin - ghost city.png +0 -0
  270. package/static/images/butterchurn-screenshots/martin - glass corridor.png +0 -0
  271. package/static/images/butterchurn-screenshots/martin - infinity _2010 update_.png +0 -0
  272. package/static/images/butterchurn-screenshots/martin - mandelbox explorer - high speed demo version.png +0 -0
  273. package/static/images/butterchurn-screenshots/martin - mucus cervix.png +0 -0
  274. package/static/images/butterchurn-screenshots/martin - reflections on black tiles.png +0 -0
  275. package/static/images/butterchurn-screenshots/martin - stormy sea _2010 update_.png +0 -0
  276. package/static/images/butterchurn-screenshots/martin - witchcraft reloaded.png +0 -0
  277. package/static/images/butterchurn-screenshots/martin _ flexi - diamond cutter _prismaticvortex_com_ - camille - i wish i wish i wish i was constrained.png +0 -0
  278. package/static/images/butterchurn-screenshots/martin _shadow harlequins shape code_ - fata morgana.png +0 -0
  279. package/static/images/butterchurn-screenshots/martin_ flexi_ fishbrain _ sto - enterstate _random mashup_.png +0 -0
  280. package/static/images/butterchurn-screenshots/sawtooth grin roam.png +0 -0
  281. package/static/images/butterchurn-screenshots/shifter - dark tides bdrv mix 2.png +0 -0
  282. 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
  283. package/static/images/butterchurn-screenshots/suksma - heretical crosscut playpen.png +0 -0
  284. package/static/images/butterchurn-screenshots/suksma - uninitialized variabowl _hydroponic chronic_.png +0 -0
  285. package/static/images/butterchurn-screenshots/suksma - vector exp 1 - couldn_t not.png +0 -0
  286. package/static/images/butterchurn-screenshots/yin - 191 - Temporal singularities.png +0 -0
  287. package/static/images/logo-512.png +0 -0
  288. package/static/images/logo.png +0 -0
  289. package/static/loukai-logo.png +0 -0
  290. package/static/screenshot-generator.html +610 -0
@@ -0,0 +1 @@
1
+ {"version":3,"mappings":"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","names":["KaraokeRenderer","canvasId","butterchurnAPI","defaultPresets","presetData","startPreset","p","error","container","resizeCanvas","containerRect","containerWidth","containerHeight","aspectRatio","displayWidth","displayHeight","metadata","url","show","generateQRCodeCanvas","__vitePreload","queue","display","lyricsData","songDuration","data","line","index","words","text","singer","isBackup","word","startTime","duration","w","wordDuration","time","oldTime","elapsed","interpolated","analyserNode","paContext","presets","reactivePresets","preset","audioData","arrayBuffer","waveformBuffer","analysisContext","event","binCount","bassEnd","midEnd","bassSum","midSum","trebleSum","totalEnergy","weightedSum","i","value","bassAvg","midAvg","trebleAvg","energyAvg","centroid","gl","analysis","bassLevel","midLevel","trebleLevel","now","nextIndex","maxAttempts","attempts","currentIndex","prevIndex","presetName","transitionTime","_effectName","allPresets","name","type","playing","startOffset","average","a","b","max","statusText","constraints","source","resolve","track","channelData","sampleRate","samplesPerWaveformPoint","totalPoints","startSample","endSample","maxVal","minVal","j","peak","signedValue","timeElapsed","samplesToShift","samplesPerPoint","bufferIndex","startIndex","endIndex","validStart","validEnd","leftPadding","rightPadding","destIndex","width","height","vocalsHeight","micHeight","gap","vocalsY","currentPositionX","centerY","firstPoint","x","normalized","y","enabled","waveformHeight","waveformY","animate","currentTime","deltaTime","frameStart","shouldProfile","currentLineIndex","targetFrameTime","padding","qrSize","bottomPadding","rightX","lineHeight","labelFontSize","songFontSize","labelText","maxWidth","songData","item","title","titleWidth","singerWidth","singerText","totalWidth","bgWidth","bgHeight","bgX","bgY","radius","textY","textX","isKJ","currentSinger","canvasWidth","canvasHeight","lines","currentLine","testLine","lineSpacing","totalHeight","currentY","skipUpcoming","activeLines","mainLines","backupLines","totalMainLines","totalBackupHeight","wrappedLineCount","backupY","animation","alpha","hasActiveLyrics","upcomingY","lowestTransitionY","_lineIndex","transition","transitionY","lineIndex","lyricLine","yPosition","finalY","prefix","glowColor","textLine","adjustedY","prefixedText","isActive","targetAlpha","newFadeDirection","progress","easedProgress","currentWidth","wordWidth","spaceWidth","centerX","isCurrentLine","spacing","_index","isActiveWord","firstLine","lastMainLine","outroLength","nextMainLine","currentLineEnd","nextLineStart","lastMainLineIndex","gapDuration","isInGap","gapProgress","barWidth","barX","barY","introDuration","introProgress","outroStartTime","outroProgress","gapStart","nextLine","startY","isReady","textColor","metrics","textWidth","textAscent","textDescent","textHeight","extraSize","borderRadius","barHeight","lineY","ballX","ballY","ctx","artist","requester","artistY","artistText","artistWidth","startX","currentPreferences","prefs","maxTimeAhead","lockedLine","isTransitioning","nextUpcomingIndex","closestStartTime","linesRendered","currentActiveLines","currentActiveEndY","upcomingLines","upcomingLine","timeToStart","upcomingPosition","activePosition","newProgress","timeUntilActive","currentMainLines","activeY","timeUntilEnd","hiddenIndex","targetColorHex","endColor","startColor","r","g","hex","defaultColor","num","CDGPlayer","PlayerInterface","opacity","_songData","cdgBuffer","mp3ArrayBuffer","currentPos","offset","positionSec","wasPlaying","render","result","imageData","bgR","bgG","bgB","overlayOpacity","scale","cdgWidth","cdgHeight","offsetX","offsetY","canvas","butterchurn","audioContext","gainNode","MicrophoneEngine","deviceId","settings","gainValue","PlayerController","kaiPlayer","shouldEnableVocalsPA","maxLyricTime","endTime","lyrics","vocalsSource","position","boundedPosition"],"ignoreList":[],"sources":["../../js/karaokeRenderer.js","../../js/cdgPlayer.js","../../js/player.js"],"sourcesContent":["// TODO: State should be passed to renderer instead of accessing globals\n\nexport class KaraokeRenderer {\n constructor(canvasId) {\n this.canvas = document.getElementById(canvasId);\n\n if (!this.canvas) {\n return;\n }\n\n this.ctx = this.canvas.getContext('2d');\n this.lyrics = null;\n this.songDuration = 0;\n this.currentTime = 0;\n this.animationFrame = null;\n this.isPlaying = false;\n\n // Time interpolation for smooth progress bar (60fps)\n this.lastReportedTime = 0;\n this.lastReportedTimestamp = performance.now();\n\n // Animation tracking for backup singers\n this.backupAnimations = new Map(); // lineIndex -> { alpha, fadeDirection, lastStateChange }\n\n // Callback for when current line's singer changes (for backup:PA feature)\n this.onSingerChange = null; // function(singer) - called when active line's singer changes\n this.lastActiveSinger = null; // Track last singer to detect changes\n\n // Lyric transition animations\n this.lyricTransitions = new Map(); // Track lyrics moving from upcoming to active\n this.hiddenDuringTransition = new Set(); // Track lines that should be hidden during transitions\n\n // Performance optimization - cache expensive calculations\n this.cachedCurrentLine = -1;\n this.lastTimeForLineCalculation = -1;\n this.lineCalculationTolerance = 0.1; // Only recalculate if time changed by 0.1s\n\n // Track upcoming lyric positioning\n this.lockedUpcomingIndex = null;\n this.lastActiveLyricsBottom = null; // Save the Y position after drawing active lyrics\n\n // Frame rate optimization\n this.frameCount = 0;\n this.maxFPS = 30; // Reduce from 60fps to 30fps for better performance\n this.frameSkip = 2; // Skip every other frame\n\n // Microphone input for waveform\n this.micStream = null;\n this.audioContext = null;\n this.analyser = null;\n this.micDataArray = null;\n this.waveformData = new Uint8Array(1440).fill(128); // 6 seconds at 240Hz (1440 pixels) - mic rolling buffer (128 = silence)\n this.micGainNode = null; // For routing mic to speakers\n this.inputDevice = 'default'; // Stored input device ID from preferences\n\n // Waveform preferences (will be set from main app)\n this.waveformPreferences = {\n enableWaveforms: false,\n micToSpeakers: true,\n enableMic: true,\n enableEffects: true,\n overlayOpacity: 0.7,\n showUpcomingLyrics: true,\n };\n\n // FPS and performance tracking\n this.fpsHistory = [];\n this.lastFrameTime = performance.now();\n this.frameUpdateTime = 0;\n\n // WebGL effects system\n this.effectsCanvas = null;\n this.effectsGL = null;\n this.musicAnalyser = null;\n this.musicFrequencyData = null;\n\n // Advanced visualization libraries\n this.butterchurn = null;\n this.currentPreset = null;\n this.presetList = [];\n this.effectType = 'butterchurn';\n // Note: Butterchurn now uses PA analyser from kaiPlayer, no separate context needed\n\n // AudioWorklet for efficient analysis\n this.musicWorkletNode = null;\n this.cachedAnalysis = { energy: 0, bass: 0, mid: 0, treble: 0, centroid: 0 };\n this.workletAvailable = false;\n this.musicAudioBuffer = null;\n this.musicSourceNode = null;\n this.vocalsWaveformData = new Uint8Array(1920).fill(128); // 8 seconds at 240Hz (1920 pixels) - vocals rendering array (128 = silence)\n this.zeroPadding = new Uint8Array(1920).fill(128); // Center value array for concatenation (128 = silence)\n this.waveformDataIndex = 0;\n\n // Vocals track waveform\n this.vocalsAudioBuffer = null;\n this.vocalsWaveformMaxLength = 480; // 8 seconds at 60fps\n this.vocalsAnalyser = null;\n this.vocalsSource = null;\n this.preCalculatedVocalsWaveform = null;\n\n // Debug audio level monitoring\n this.lastAudioDebugTime = 0;\n this.audioDebugInterval = 1000; // Log every 1 second for testing\n this.lastConditionsDebugTime = 0;\n\n // QR code for server URL\n this.qrCodeCanvas = null;\n this.showQrCode = false;\n this.serverUrl = null;\n\n // Queue display\n this.queueItems = [];\n this.displayQueue = true;\n\n // Karaoke visual settings scaled for 1080p\n this.settings = {\n fontSize: 80, // Scaled up for 1080p (was 40 for ~800px)\n fontFamily: 'bold Arial, sans-serif',\n lineHeight: 140, // Increased spacing between lines\n textColor: '#ffffff',\n activeColor: '#00BFFF', // Light blue for active lines (easier to read)\n upcomingColor: '#888888', // Gray for upcoming lines\n backupColor: '#DAA520', // Golden color for backup singer lines\n lyricTransitionDuration: 0.3, // Animation duration in seconds (300ms)\n lyricTransitionStartBefore: 0.3, // Start animation this many seconds before active (300ms)\n backupActiveColor: '#FFD700', // Brighter gold when active\n // Singer type colors\n singerBColor: '#EF4444', // Red for Singer B (duet partner)\n duetColor: '#22C55E', // Green for duet (both singers)\n backupPAColor: '#FFA500', // Orange for backup:PA (brighter than gold backup)\n backgroundColor: '#1a1a1a',\n shadowColor: '#000000',\n linesVisible: 1, // Show only current line\n maxWidth: 0.9, // 90% of canvas width for text\n progressBarHeight: 30, // Taller progress bar\n progressBarColor: '#007acc',\n progressBarBg: '#333333',\n progressBarMargin: 100, // More space between progress bar and lyrics\n\n // Backup singer animation settings\n backupFadeDuration: 0.8, // seconds to fade in/out\n backupMaxAlpha: 0.5, // maximum opacity for backup singers (50% - subtle background feel)\n backupMinAlpha: 0.0, // minimum opacity (fully transparent)\n backupAnimationEasing: 'ease-out', // animation curve\n\n // Microphone waveform settings\n waveformHeight: 80, // Height of the waveform area\n waveformColor: '#00ff00', // Green waveform\n waveformBackgroundColor: '#333333',\n waveformCurrentPosition: 0.75, // Position of current time (75% from left)\n\n // Vocals waveform settings\n vocalsWaveformHeight: 60, // Slightly smaller than mic waveform\n vocalsWaveformColor: '#00bfff', // Blue waveform for vocals to match lyrics\n vocalsWaveformGap: 10, // Gap between vocals and mic waveforms\n };\n\n this.setupCanvas();\n this.setupAdvancedVisualizations();\n this.setupResponsiveCanvas();\n this.startAnimation();\n }\n\n setupAdvancedVisualizations() {\n // Create offscreen canvas for effects\n this.effectsCanvas = document.createElement('canvas');\n this.effectsCanvas.width = 1920;\n this.effectsCanvas.height = 1080;\n\n try {\n // Try to load Butterchurn (Milkdrop visualizations) from global variables\n if (typeof window !== 'undefined' && window.butterchurn && window.butterchurnPresets) {\n this.effectsGL =\n this.effectsCanvas.getContext('webgl2') || this.effectsCanvas.getContext('webgl');\n if (this.effectsGL) {\n // Try different API patterns for Butterchurn\n let butterchurnAPI = null;\n if (typeof window.butterchurn.createVisualizer === 'function') {\n butterchurnAPI = window.butterchurn;\n } else if (\n window.butterchurn.default &&\n typeof window.butterchurn.default.createVisualizer === 'function'\n ) {\n butterchurnAPI = window.butterchurn.default;\n } else if (typeof window.butterchurn === 'function') {\n // Maybe it's a constructor function\n butterchurnAPI = window.butterchurn;\n }\n\n if (!butterchurnAPI || typeof butterchurnAPI.createVisualizer !== 'function') {\n console.error(\n 'Butterchurn createVisualizer not found. Available methods:',\n butterchurnAPI ? Object.keys(butterchurnAPI) : 'none'\n );\n throw new Error('Butterchurn API not compatible');\n }\n\n // Create waveform audio context if needed (used as dummy for butterchurn)\n if (!this.waveformAudioContext) {\n this.waveformAudioContext = new (window.AudioContext || window.webkitAudioContext)();\n }\n\n // Initialize Butterchurn with the correct API: createVisualizer(audioContext, canvas, options)\n // Note: Audio comes from PA analyser via setVisualizationAnalyser(), not from this context\n this.butterchurn = butterchurnAPI.createVisualizer(\n this.waveformAudioContext,\n this.effectsCanvas,\n {\n width: 1920,\n height: 1080,\n mesh_width: 128, // Lower for performance\n mesh_height: 72, // Lower for performance\n fps: 30, // Match our target framerate\n }\n );\n\n // If we already have music loaded, decode it for Butterchurn\n this.tryDecodeStoredAudioForButterchurn();\n\n // Get available presets\n this.presetList = Object.keys(window.butterchurnPresets.getPresets());\n\n // Load highly reactive presets that respond strongly to audio\n const defaultPresets = [\n 'Rovastar - Fractopia', // Very reactive to bass/drums\n 'Rovastar - Altars Of Madness (Krash Mix)', // High energy response\n 'Rovastar - Tunnel Runner', // Fast visual response\n 'martin - disco ball', // Classic reactive preset\n 'Krash - The Neverending Explosion', // Explosive audio response\n 'flexi - mindblob mix', // Good bass response\n 'Rovastar - Crystal High', // Sharp audio reactions\n 'martin - being & time', // Dynamic movement\n ];\n\n // Only load a default preset if no preset is currently selected\n // This prevents auto-resetting effects when new songs start\n if (!this.currentPreset) {\n const startPreset =\n defaultPresets.find((p) => this.presetList.includes(p)) || this.presetList[0];\n if (startPreset) {\n const presetData = window.butterchurnPresets.getPresets()[startPreset];\n this.butterchurn.loadPreset(presetData, 0.0); // 0 second transition\n this.currentPreset = startPreset;\n }\n } else {\n // If we already have a current preset, reload it to maintain continuity\n if (this.presetList.includes(this.currentPreset)) {\n const presetData = window.butterchurnPresets.getPresets()[this.currentPreset];\n this.butterchurn.loadPreset(presetData, 0.0);\n }\n }\n\n this.effectType = 'butterchurn';\n\n // Check if we have music loaded but no Butterchurn buffer yet\n if (this.musicAudioBuffer && !this.butterchurnAudioBuffer) {\n // We need to get the original audio data, but AudioBuffer doesn't give us access\n // This is a complex issue that requires storing the original ArrayBuffer\n // For now, let's add a flag to trigger re-loading\n this.needsButterchurnAudioDecode = true;\n }\n }\n } else {\n console.warn('Butterchurn libraries not available, effects disabled');\n throw new Error('Butterchurn not available');\n }\n } catch (error) {\n console.error('Butterchurn failed to load, effects disabled:', error);\n this.effectType = 'disabled';\n }\n\n if (!this.effectsGL) {\n console.warn('WebGL not available, effects disabled');\n }\n }\n\n setupCanvas() {\n // Canvas size is ALWAYS 1920x1080 (1080p)\n // CSS controls how it stretches to fit the container\n this.canvas.width = 1920;\n this.canvas.height = 1080;\n\n // Set default font\n this.ctx.font = `${this.settings.fontSize}px ${this.settings.fontFamily}`;\n this.ctx.textAlign = 'left';\n this.ctx.textBaseline = 'middle';\n }\n\n setupResponsiveCanvas() {\n // Get container reference for ResizeObserver\n const container = this.canvas.parentElement;\n\n // Function to maintain 16:9 aspect ratio (1920:1080) while scaling to fit container\n const resizeCanvas = () => {\n if (!container) return;\n\n const containerRect = container.getBoundingClientRect();\n const containerWidth = containerRect.width;\n const containerHeight = containerRect.height;\n\n // Skip if container has no dimensions yet\n if (containerWidth === 0 || containerHeight === 0) {\n // Schedule another attempt after DOM settles\n setTimeout(() => resizeCanvas(), 100);\n return;\n }\n\n // 16:9 aspect ratio (1920/1080 = 1.7777...)\n const aspectRatio = 16 / 9;\n\n let displayWidth, displayHeight;\n\n // Calculate size that fits container while maintaining aspect ratio\n if (containerWidth / containerHeight > aspectRatio) {\n // Container is wider than 16:9, fit by height\n displayHeight = containerHeight;\n displayWidth = displayHeight * aspectRatio;\n } else {\n // Container is taller than 16:9, fit by width\n displayWidth = containerWidth;\n displayHeight = displayWidth / aspectRatio;\n }\n\n // Set CSS size to maintain proportions\n this.canvas.style.width = displayWidth + 'px';\n this.canvas.style.height = displayHeight + 'px';\n };\n\n // Initial resize\n resizeCanvas();\n\n // Double-check sizing after DOM fully settles\n setTimeout(() => resizeCanvas(), 100);\n requestAnimationFrame(() => resizeCanvas());\n\n // Resize on window resize\n window.addEventListener('resize', resizeCanvas);\n\n // Watch container for size changes (e.g., sidebar drawer open/close)\n // This catches layout changes that don't trigger window resize events\n if (container && typeof ResizeObserver !== 'undefined') {\n this.resizeObserver = new ResizeObserver(() => {\n resizeCanvas();\n });\n this.resizeObserver.observe(container);\n }\n\n // Store reference to remove listener on destroy\n this.resizeHandler = resizeCanvas;\n }\n\n setSongMetadata(metadata) {\n // Store song metadata for display when not playing\n this.songMetadata = metadata || {};\n }\n\n /**\n * Set server URL and generate QR code\n * @param {string} url - Server URL\n * @param {boolean} show - Whether to show QR code\n */\n async setServerQRCode(url, show) {\n this.serverUrl = url;\n this.showQrCode = show;\n\n if (url && show) {\n try {\n // Dynamically import QR code generator\n const { generateQRCodeCanvas } = await import('../utils/qrCodeGenerator.js');\n this.qrCodeCanvas = await generateQRCodeCanvas(url, 150);\n } catch (error) {\n console.error('Error generating QR code:', error);\n this.qrCodeCanvas = null;\n }\n } else {\n this.qrCodeCanvas = null;\n }\n }\n\n /**\n * Set queue items and display setting\n * @param {Array} queue - Array of queue items with title, artist, requester\n * @param {boolean} display - Whether to display queue\n */\n setQueueDisplay(queue, display) {\n this.queueItems = queue || [];\n this.displayQueue = display !== false;\n }\n\n loadLyrics(lyricsData, songDuration = 0) {\n // Store original lyrics data for outro detection\n this.originalLyricsData = lyricsData || [];\n // Store filtered lyrics for display\n this.lyrics = this.parseLyricsData(lyricsData);\n this.songDuration = songDuration;\n }\n\n parseLyricsData(data) {\n if (!data || !Array.isArray(data)) return [];\n\n // Filter out disabled lines for playback (backup lines are still included)\n const enabledData = data.filter((line) => line.disabled !== true);\n\n return enabledData\n .map((line, index) => {\n if (typeof line === 'object' && line !== null) {\n const words = this.parseWordsFromLine(line);\n const text = line.text || line.lyrics || line.content || line.lyric || '';\n // Support new singer field format (backup, backup:PA, B, duet, etc.)\n // Falls back to legacy backup boolean for compatibility\n const singer = line.singer || (line.backup === true ? 'backup' : null);\n const isBackup = singer?.startsWith('backup') || false;\n return {\n id: index,\n startTime: line.start || line.time || line.start_time || index * 3,\n endTime: line.end || line.end_time || (line.start || line.time || index * 3) + 3,\n text: text,\n words: words,\n singer: singer, // New: singer field (null, 'A', 'B', 'backup', 'backup:PA', 'duet')\n isBackup: isBackup, // Derived from singer field for backward compatibility\n };\n } else {\n // Simple string - create word timing estimates\n const text = line || '';\n const words = this.estimateWordTiming(text, index * 3);\n return {\n id: index,\n startTime: index * 3,\n endTime: index * 3 + 3,\n text: text,\n words: words,\n isBackup: false,\n };\n }\n })\n .filter((line) => line.text.trim().length > 0);\n }\n\n parseWordsFromLine(line) {\n // If the line has word-level timing data, use it\n if (line.words && Array.isArray(line.words)) {\n return line.words.map((word) => ({\n text: word.t || word.text || word.word || '', // word.word for Whisper output\n startTime: word.s || word.start || word.startTime || 0,\n endTime: word.e || word.end || word.endTime || 0,\n }));\n }\n\n // Otherwise estimate word timing\n const text = line.text || line.lyrics || line.content || line.lyric || '';\n const startTime = line.start || line.time || line.start_time || 0;\n const endTime = line.end || line.end_time || startTime + 3;\n const duration = endTime - startTime;\n\n return this.estimateWordTiming(text, startTime, duration);\n }\n\n estimateWordTiming(text, startTime, duration = 3) {\n const words = text.split(/\\s+/).filter((w) => w.length > 0);\n if (words.length === 0) return [];\n\n const wordDuration = duration / words.length;\n\n return words.map((word, index) => ({\n text: word,\n startTime: startTime + index * wordDuration,\n endTime: startTime + (index + 1) * wordDuration,\n }));\n }\n\n setCurrentTime(time) {\n const oldTime = this.currentTime;\n this.currentTime = time;\n\n // Track for interpolation\n this.lastReportedTime = time;\n this.lastReportedTimestamp = performance.now();\n\n // If time jumped significantly and we're playing, restart music analysis from new position\n if (this.isPlaying && Math.abs(time - oldTime) > 1.0) {\n // 1 second threshold - restart butterchurn analysis to stay in sync\n // Stop first, then delay restart slightly to ensure it happens after PA sources restart\n this.stopMusicAnalysis();\n\n // Delay restart by a small amount to allow PA audio sources to stabilize first\n setTimeout(() => {\n if (this.isPlaying) {\n this.startMusicAnalysis();\n }\n }, 50); // 50ms delay\n }\n }\n\n /**\n * Get interpolated current time for smooth 60fps progress bars\n * When playing, calculates time based on elapsed time since last update\n */\n getInterpolatedTime() {\n if (!this.isPlaying) {\n return this.currentTime;\n }\n\n // Calculate elapsed time since last report\n const now = performance.now();\n const elapsed = (now - this.lastReportedTimestamp) / 1000; // Convert to seconds\n const interpolated = this.lastReportedTime + elapsed;\n\n // Don't exceed song duration\n return Math.min(interpolated, this.songDuration || Infinity);\n }\n\n /**\n * Set the analyser node for butterchurn visualization\n * This is provided by the PA audio context from kaiPlayer\n * @param {AnalyserNode} analyserNode - The PA analyser node\n */\n setVisualizationAnalyser(analyserNode) {\n if (!analyserNode) return;\n\n try {\n console.log('🎨 Connecting butterchurn to PA analyser');\n\n // Check if butterchurn was created with a different context\n // If so, recreate it with the PA context from the analyser\n const paContext = analyserNode.context;\n\n if (this.butterchurn && this.butterchurn.audioContext !== paContext) {\n console.log('⚠️ Butterchurn context mismatch - recreating with PA context');\n\n // Destroy old butterchurn\n if (this.butterchurn.destroy) {\n this.butterchurn.destroy();\n }\n\n // Get butterchurn API\n let butterchurnAPI = null;\n if (typeof window.butterchurn.createVisualizer === 'function') {\n butterchurnAPI = window.butterchurn;\n } else if (\n window.butterchurn.default &&\n typeof window.butterchurn.default.createVisualizer === 'function'\n ) {\n butterchurnAPI = window.butterchurn.default;\n }\n\n if (butterchurnAPI) {\n // Recreate with PA context\n this.butterchurn = butterchurnAPI.createVisualizer(paContext, this.effectsCanvas, {\n width: 1920,\n height: 1080,\n mesh_width: 128,\n mesh_height: 72,\n fps: 30,\n });\n\n // Reload current preset if available\n if (this.currentPreset && window.butterchurnPresets) {\n const presets = window.butterchurnPresets.getPresets();\n if (presets[this.currentPreset]) {\n this.butterchurn.loadPreset(presets[this.currentPreset], 0.0);\n }\n }\n }\n }\n\n // Connect analyser to butterchurn\n if (this.butterchurn) {\n this.butterchurn.connectAudio(analyserNode);\n console.log('✅ Butterchurn connected to PA analyser successfully');\n }\n } catch (error) {\n console.error('❌ Failed to connect butterchurn to analyser:', error);\n console.error(' Error type:', error.name);\n console.error(' Error message:', error.message);\n }\n }\n\n // setMusicAudio() removed - butterchurn now uses PA analyser from kaiPlayer\n // Connected via setVisualizationAnalyser() during song load\n\n reinitializeButterchurn() {\n try {\n // Destroy the old Butterchurn instance if it exists\n if (this.butterchurn && this.butterchurn.destroy) {\n this.butterchurn.destroy();\n }\n\n // Clear old references\n this.butterchurn = null;\n\n // Get the Butterchurn API\n let butterchurnAPI = null;\n if (typeof window.butterchurn.createVisualizer === 'function') {\n butterchurnAPI = window.butterchurn;\n } else if (\n window.butterchurn.default &&\n typeof window.butterchurn.default.createVisualizer === 'function'\n ) {\n butterchurnAPI = window.butterchurn.default;\n } else if (typeof window.butterchurn === 'function') {\n butterchurnAPI = window.butterchurn;\n }\n\n if (!butterchurnAPI || typeof butterchurnAPI.createVisualizer !== 'function') {\n console.error('Butterchurn createVisualizer not found during reinit');\n return;\n }\n\n // Create fresh Butterchurn instance (uses waveformAudioContext as dummy)\n // Note: Audio comes from PA analyser via setVisualizationAnalyser(), not from this context\n if (!this.waveformAudioContext) {\n this.waveformAudioContext = new (window.AudioContext || window.webkitAudioContext)();\n }\n\n this.butterchurn = butterchurnAPI.createVisualizer(\n this.waveformAudioContext,\n this.effectsCanvas,\n {\n width: 1920,\n height: 1080,\n mesh_width: 128,\n mesh_height: 72,\n fps: 30,\n }\n );\n\n // Reload presets\n if (window.butterchurnPresets && window.butterchurnPresets.getPresets) {\n this.presetList = Object.keys(window.butterchurnPresets.getPresets());\n\n // Restore the current preset if it exists, otherwise load a reactive preset\n if (this.currentPreset && this.presetList.includes(this.currentPreset)) {\n const presetData = window.butterchurnPresets.getPresets()[this.currentPreset];\n this.butterchurn.loadPreset(presetData, 0.0);\n } else {\n // Only load a reactive preset if no current preset exists\n const reactivePresets = [\n 'Geiss - Pulse Vertex v1.02',\n 'Rovastar & Geiss - Dynamic Noise v2.0',\n 'martin - volume bar spectrogram v1.0',\n ];\n\n for (const preset of reactivePresets) {\n if (this.presetList.includes(preset)) {\n this.butterchurn.loadPreset(window.butterchurnPresets.getPresets()[preset], 0.0);\n this.currentPreset = preset;\n break;\n }\n }\n }\n }\n } catch (error) {\n console.error('Failed to reinitialize Butterchurn:', error);\n }\n }\n\n async setVocalsAudio(audioData) {\n try {\n if (!this.waveformAudioContext) {\n this.waveformAudioContext = new (window.AudioContext || window.webkitAudioContext)();\n }\n\n // Ensure audioData is an ArrayBuffer\n let arrayBuffer;\n if (audioData instanceof ArrayBuffer) {\n arrayBuffer = audioData;\n } else if (audioData && audioData.buffer instanceof ArrayBuffer) {\n // Handle Node.js Buffer-like objects (which have a .buffer property)\n arrayBuffer = audioData.buffer.slice(\n audioData.byteOffset,\n audioData.byteOffset + audioData.byteLength\n );\n } else if (audioData instanceof Uint8Array) {\n // Handle Uint8Array\n arrayBuffer = audioData.buffer.slice(\n audioData.byteOffset,\n audioData.byteOffset + audioData.byteLength\n );\n } else {\n return; // Unexpected audio data type\n }\n\n // Decode the audio data\n this.vocalsAudioBuffer = await this.waveformAudioContext.decodeAudioData(arrayBuffer);\n\n // Pre-calculate waveform for smooth animation\n this.preCalculateVocalsWaveform();\n } catch {\n // Failed to load vocals audio for waveform\n }\n }\n\n async setupMusicAnalysis(waveformBuffer, arrayBuffer) {\n if (!this.effectsGL) return;\n\n try {\n // Use Butterchurn's AudioContext for live music analysis (for sync with playback)\n const analysisContext = this.butterchurnAudioContext || this.waveformAudioContext;\n\n // Store waveform buffer for UI visualization\n this.musicAudioBuffer = waveformBuffer;\n\n // If we have the shared Butterchurn context, decode audio for it too (if not already done)\n if (this.butterchurnAudioContext && !this.butterchurnAudioBuffer && arrayBuffer) {\n try {\n this.butterchurnAudioBuffer = await this.butterchurnAudioContext.decodeAudioData(\n arrayBuffer.slice(0)\n );\n } catch (error) {\n console.warn('Failed to decode audio for Butterchurn context:', error);\n }\n }\n\n // Try to use AudioWorklet for better performance (use the analysis context)\n try {\n await analysisContext.audioWorklet.addModule('./js/musicAnalysisWorklet.js');\n this.workletAvailable = true;\n } catch {\n console.warn('AudioWorklet not available, falling back to AnalyserNode');\n this.workletAvailable = false;\n }\n\n if (this.workletAvailable) {\n // Create the worklet node but don't connect it yet\n this.musicWorkletNode = new AudioWorkletNode(analysisContext, 'music-analysis-processor');\n\n // Listen for analysis results\n this.musicWorkletNode.port.onmessage = (event) => {\n if (event.data.type === 'analysis') {\n this.cachedAnalysis = event.data.data;\n }\n };\n } else {\n // Create analyser but don't connect it yet\n this.musicAnalyser = analysisContext.createAnalyser();\n this.musicAnalyser.fftSize = 512;\n this.musicAnalyser.smoothingTimeConstant = 0.8;\n this.musicFrequencyData = new Uint8Array(this.musicAnalyser.frequencyBinCount);\n }\n } catch (error) {\n console.warn('Failed to setup music analysis:', error);\n }\n }\n\n async tryDecodeStoredAudioForButterchurn() {\n // If we already have music loaded but Butterchurn doesn't have the audio buffer\n if (\n this.butterchurn &&\n this.butterchurnAudioContext &&\n this.originalAudioArrayBuffer &&\n !this.butterchurnAudioBuffer\n ) {\n try {\n this.butterchurnAudioBuffer = await this.butterchurnAudioContext.decodeAudioData(\n this.originalAudioArrayBuffer.slice(0)\n );\n\n // Also update the music buffer reference for UI if we don't have it yet\n if (!this.musicAudioBuffer && this.waveformAudioContext) {\n try {\n this.musicAudioBuffer = await this.waveformAudioContext.decodeAudioData(\n this.originalAudioArrayBuffer.slice(0)\n );\n } catch (error) {\n console.warn('Failed to decode audio for waveform:', error);\n }\n }\n } catch (error) {\n console.warn('Failed to decode stored audio for Butterchurn context:', error);\n }\n }\n }\n\n analyzeMusicFrequencies() {\n // Use cached results from AudioWorklet if available\n if (this.workletAvailable && this.musicWorkletNode) {\n return this.cachedAnalysis;\n }\n\n // Fallback to traditional analysis\n if (!this.musicAnalyser || !this.musicFrequencyData) {\n return { energy: 0, bass: 0, mid: 0, treble: 0, centroid: 0 };\n }\n\n // Get frequency data (expensive operation on main thread)\n this.musicAnalyser.getByteFrequencyData(this.musicFrequencyData);\n\n const binCount = this.musicFrequencyData.length;\n const bassEnd = Math.floor(binCount * 0.1); // 0-10% (bass)\n const midEnd = Math.floor(binCount * 0.4); // 10-40% (mids)\n // 40-100% is treble\n\n let bassSum = 0,\n midSum = 0,\n trebleSum = 0,\n totalEnergy = 0;\n let weightedSum = 0; // for spectral centroid\n\n for (let i = 0; i < binCount; i++) {\n const value = this.musicFrequencyData[i] / 255.0; // Normalize to 0-1\n totalEnergy += value;\n weightedSum += value * i;\n\n if (i < bassEnd) {\n bassSum += value;\n } else if (i < midEnd) {\n midSum += value;\n } else {\n trebleSum += value;\n }\n }\n\n // Calculate averages\n const bassAvg = bassEnd > 0 ? bassSum / bassEnd : 0;\n const midAvg = midEnd - bassEnd > 0 ? midSum / (midEnd - bassEnd) : 0;\n const trebleAvg = binCount - midEnd > 0 ? trebleSum / (binCount - midEnd) : 0;\n const energyAvg = binCount > 0 ? totalEnergy / binCount : 0;\n\n // Calculate spectral centroid (normalized)\n const centroid = totalEnergy > 0 ? weightedSum / totalEnergy / binCount : 0;\n\n return {\n energy: Math.min(energyAvg * 20, 1.0), // Scale for visual effects\n bass: Math.min(bassAvg * 30, 1.0),\n mid: Math.min(midAvg * 25, 1.0),\n treble: Math.min(trebleAvg * 20, 1.0),\n centroid: centroid,\n };\n }\n\n renderWebGLEffects() {\n if (!this.effectsGL) {\n return;\n }\n\n // Clear effects canvas if disabled\n if (!this.waveformPreferences.enableEffects) {\n const gl = this.effectsGL;\n gl.clearColor(0, 0, 0, 1);\n gl.clear(gl.COLOR_BUFFER_BIT);\n return;\n }\n\n const _gl = this.effectsGL;\n const analysis = this.analyzeMusicFrequencies();\n\n // if (Math.random() < 0.01) { // Debug occasionally\n // }\n\n // Use Butterchurn for background effects\n if (this.effectType === 'butterchurn' && this.butterchurn) {\n try {\n // Convert our analysis data to Butterchurn's expected format\n const audioData = {\n timeArray: new Uint8Array(1024), // Time domain data (not used much)\n freqArray: new Uint8Array(1024), // Frequency domain data\n };\n\n // Fill frequency data based on our analysis\n // Butterchurn expects 0-255 values\n const bassLevel = Math.floor(analysis.bass * 255);\n const midLevel = Math.floor(analysis.mid * 255);\n const trebleLevel = Math.floor(analysis.treble * 255);\n\n // Distribute frequency data across the array\n for (let i = 0; i < 1024; i++) {\n if (i < 341) {\n // Bass frequencies (0-33% of spectrum)\n audioData.freqArray[i] = bassLevel;\n } else if (i < 682) {\n // Mid frequencies (33-66% of spectrum)\n audioData.freqArray[i] = midLevel;\n } else {\n // Treble frequencies (66-100% of spectrum)\n audioData.freqArray[i] = trebleLevel;\n }\n }\n\n // Render Butterchurn frame\n this.butterchurn.render();\n\n // Debug audio levels periodically\n this.debugAudioLevels();\n\n // Ensure Butterchurn source is running if we're playing but source is missing\n if (\n this.isPlaying &&\n this.butterchurnAnalyser &&\n !this.butterchurnSourceNode &&\n this.butterchurnAudioBuffer\n ) {\n this.startMusicAnalysis();\n } else if (\n this.isPlaying &&\n !this.butterchurnSourceNode &&\n this.musicAudioBuffer &&\n !this.butterchurnAudioBuffer\n ) {\n // Fix missing Butterchurn audio buffer - decode the music for Butterchurn\n try {\n // Get the original audio data from musicAudioBuffer\n // We need to re-decode since AudioBuffer can't be transferred between contexts\n // This is a limitation - we'd need the original ArrayBuffer, but for now let's skip this complex case\n } catch (error) {\n console.warn('Failed to decode audio for Butterchurn:', error);\n }\n } else if (this.isPlaying && !this.butterchurnSourceNode) {\n // Debug why auto-fix isn't triggering (limit frequency)\n const now = performance.now();\n if (now - this.lastConditionsDebugTime > 2000) {\n this.lastConditionsDebugTime = now;\n }\n }\n } catch (error) {\n console.error('Butterchurn render failed:', error);\n }\n }\n }\n\n // Preset management methods\n switchToNextPreset() {\n if (this.effectType === 'butterchurn' && this.butterchurn && this.presetList.length) {\n const currentIndex = this.presetList.indexOf(this.currentPreset);\n let nextIndex = (currentIndex + 1) % this.presetList.length;\n\n // Skip disabled effects\n const maxAttempts = this.presetList.length;\n let attempts = 0;\n while (attempts < maxAttempts && this.isEffectDisabled(this.presetList[nextIndex])) {\n nextIndex = (nextIndex + 1) % this.presetList.length;\n attempts++;\n }\n\n // Only switch if we found an enabled effect\n if (!this.isEffectDisabled(this.presetList[nextIndex])) {\n this.switchToPreset(this.presetList[nextIndex]);\n }\n }\n }\n\n switchToPreviousPreset() {\n if (this.effectType === 'butterchurn' && this.butterchurn && this.presetList.length) {\n const currentIndex = this.presetList.indexOf(this.currentPreset);\n let prevIndex = currentIndex <= 0 ? this.presetList.length - 1 : currentIndex - 1;\n\n // Skip disabled effects\n const maxAttempts = this.presetList.length;\n let attempts = 0;\n while (attempts < maxAttempts && this.isEffectDisabled(this.presetList[prevIndex])) {\n prevIndex = prevIndex <= 0 ? this.presetList.length - 1 : prevIndex - 1;\n attempts++;\n }\n\n // Only switch if we found an enabled effect\n if (!this.isEffectDisabled(this.presetList[prevIndex])) {\n this.switchToPreset(this.presetList[prevIndex]);\n }\n }\n }\n\n switchToPreset(presetName, transitionTime = 2.0) {\n if (\n this.effectType !== 'butterchurn' ||\n !this.butterchurn ||\n !this.presetList.includes(presetName)\n ) {\n console.warn('Cannot switch to preset:', presetName);\n return;\n }\n\n try {\n const presetData = window.butterchurnPresets.getPresets()[presetName];\n this.butterchurn.loadPreset(presetData, transitionTime);\n this.currentPreset = presetName;\n } catch (error) {\n console.error('Failed to switch preset:', error);\n }\n }\n\n isEffectDisabled(_effectName) {\n // TODO: Get disabled effects from Context/props instead\n return false;\n }\n\n setButterchurnPreset(presetData, transitionTime = 1.0) {\n if (this.effectType !== 'butterchurn' || !this.butterchurn) {\n console.warn('Cannot set butterchurn preset - butterchurn not active');\n return false;\n }\n\n try {\n this.butterchurn.loadPreset(presetData, transitionTime);\n // Find the preset name for tracking\n if (window.butterchurnPresets) {\n const allPresets = window.butterchurnPresets.getPresets();\n for (const [name, preset] of Object.entries(allPresets)) {\n if (preset === presetData) {\n this.currentPreset = name;\n break;\n }\n }\n }\n return true;\n } catch (error) {\n console.error('Failed to set butterchurn preset:', error);\n return false;\n }\n }\n\n getAvailablePresets() {\n return this.presetList;\n }\n\n getCurrentPreset() {\n return this.currentPreset;\n }\n\n switchEffectType(type) {\n if (type === 'butterchurn') {\n this.effectType = type;\n }\n }\n\n setPlaying(playing) {\n this.isPlaying = playing;\n\n // Start/stop microphone capture based on playing state\n if (playing) {\n this.startMicrophoneCapture();\n this.startMusicAnalysis();\n } else {\n this.stopMicrophoneCapture();\n this.stopMusicAnalysis();\n }\n }\n\n startMusicAnalysis() {\n if (!this.musicAudioBuffer) {\n return;\n }\n if (!this.effectsGL && !this.butterchurn) {\n return;\n }\n\n // NOTE: We only use Butterchurn context for actual audio playback.\n // The waveform context is used only for UI visualization (no playback).\n\n try {\n // Stop any existing analysis\n this.stopMusicAnalysis();\n\n // Start offline audio analysis for Butterchurn (NO PLAYBACK)\n if (this.butterchurn && this.butterchurnAudioBuffer && this.butterchurnAudioContext) {\n try {\n // Stop any existing Butterchurn source\n if (this.butterchurnSourceNode) {\n this.butterchurnSourceNode.disconnect();\n this.butterchurnSourceNode = null;\n }\n\n // Create new Butterchurn source node for ANALYSIS ONLY\n this.butterchurnSourceNode = this.butterchurnAudioContext.createBufferSource();\n this.butterchurnSourceNode.buffer = this.butterchurnAudioBuffer;\n\n // IMPORTANT: DO NOT connect to destination - this eliminates audio playback!\n // Connect only to analysers for frequency analysis (no audio output)\n\n // Connect to debug analyser for monitoring\n if (this.butterchurnAnalyser) {\n this.butterchurnSourceNode.connect(this.butterchurnAnalyser);\n }\n\n // Connect to Butterchurn's internal audio processing\n // Butterchurn should have its own analyser that we can connect to\n if (this.butterchurn && this.butterchurn.connectAudio) {\n // Create dedicated analyser for Butterchurn visualization\n this.butterchurnVisualAnalyser = this.butterchurnAudioContext.createAnalyser();\n this.butterchurnVisualAnalyser.fftSize = 2048;\n this.butterchurnVisualAnalyser.smoothingTimeConstant = 0.8;\n\n // Connect audio source to Butterchurn's analyser\n this.butterchurnSourceNode.connect(this.butterchurnVisualAnalyser);\n\n // Give Butterchurn the analyser for visualization\n this.butterchurn.connectAudio(this.butterchurnVisualAnalyser);\n }\n\n // Start from current time position (analysis only, no audio output)\n // Ensure offset is never negative to avoid RangeError\n const startOffset = Math.max(0, this.currentTime);\n\n // Track when we started and from what offset for drift detection\n this.butterchurnStartTime = this.butterchurnAudioContext.currentTime;\n this.butterchurnStartOffset = startOffset;\n\n this.butterchurnSourceNode.start(0, startOffset);\n } catch (error) {\n console.warn('Failed to start Butterchurn offline analysis:', error);\n }\n }\n } catch (error) {\n console.error('Failed to start music analysis:', error);\n }\n }\n\n stopMusicAnalysis() {\n // Note: We no longer create musicSourceNode from waveform context.\n // Only Butterchurn context is used for analysis.\n\n // Stop Butterchurn source (AudioBufferSourceNode can only be used once)\n if (this.butterchurnSourceNode) {\n try {\n this.butterchurnSourceNode.stop();\n this.butterchurnSourceNode.disconnect();\n } catch {\n // Source may already be stopped\n }\n this.butterchurnSourceNode = null;\n }\n\n // Reset tracking variables\n this.butterchurnStartTime = 0;\n this.butterchurnStartOffset = 0;\n\n // Clear any stored analyser references that Butterchurn might be holding\n if (this.butterchurnVisualAnalyser) {\n this.butterchurnVisualAnalyser = null;\n }\n\n // Clear cached analysis when stopped\n this.cachedAnalysis = { energy: 0, bass: 0, mid: 0, treble: 0, centroid: 0 };\n }\n\n debugAudioLevels() {\n const now = performance.now();\n if (now - this.lastAudioDebugTime < this.audioDebugInterval) return;\n this.lastAudioDebugTime = now;\n\n if (this.butterchurnAnalyser && this.butterchurnFrequencyData) {\n this.butterchurnAnalyser.getByteFrequencyData(this.butterchurnFrequencyData);\n const sum = this.butterchurnFrequencyData.reduce((a, b) => a + b, 0);\n const average = sum / this.butterchurnFrequencyData.length;\n const max = Math.max(...this.butterchurnFrequencyData);\n\n // `Source: ${this.butterchurnSourceNode ? 'active' : 'none'}`);\n\n // Also update status bar for visual feedback\n const statusText = document.getElementById('statusText');\n if (statusText) {\n statusText.textContent = `Audio: Avg=${average.toFixed(0)} Max=${max} State=${this.butterchurnAudioContext ? this.butterchurnAudioContext.state : 'none'}`;\n }\n }\n }\n\n async startMicrophoneCapture() {\n if (!this.waveformPreferences.enableMic) return;\n\n try {\n // Use stored input device from preferences\n const constraints = {\n audio: this.inputDevice\n ? {\n deviceId: { exact: this.inputDevice },\n }\n : true,\n };\n\n this.micStream = await navigator.mediaDevices.getUserMedia(constraints);\n this.audioContext = new (window.AudioContext || window.webkitAudioContext)();\n this.analyser = this.audioContext.createAnalyser();\n\n const source = this.audioContext.createMediaStreamSource(this.micStream);\n source.connect(this.analyser);\n\n // Create gain node for analysis only - NEVER route to speakers\n // (kaiPlayer handles actual microphone audio routing to PA/IEM outputs)\n this.micGainNode = this.audioContext.createGain();\n this.micGainNode.gain.setValueAtTime(0.5, this.audioContext.currentTime);\n source.connect(this.micGainNode);\n\n // DO NOT connect to speakers - this audioContext uses default output device\n // and would bypass PA routing. kaiPlayer handles all mic-to-speaker routing.\n\n this.analyser.fftSize = 256;\n this.micDataArray = new Uint8Array(this.analyser.frequencyBinCount);\n\n // Give the microphone a moment to stabilize before processing\n await new Promise((resolve) => setTimeout(resolve, 150));\n\n // Microphone capture started\n } catch {\n // Could not start microphone capture\n }\n }\n\n stopMicrophoneCapture() {\n if (this.micStream) {\n this.micStream.getTracks().forEach((track) => track.stop());\n this.micStream = null;\n }\n\n if (this.micGainNode) {\n this.micGainNode.disconnect();\n this.micGainNode = null;\n }\n\n if (this.audioContext) {\n this.audioContext.close();\n this.audioContext = null;\n }\n\n this.analyser = null;\n this.micDataArray = null;\n this.waveformData.fill(128); // Fill with center value to avoid flatline\n }\n\n preCalculateVocalsWaveform() {\n if (!this.vocalsAudioBuffer) return;\n\n const channelData = this.vocalsAudioBuffer.getChannelData(0);\n const sampleRate = this.vocalsAudioBuffer.sampleRate;\n const duration = this.vocalsAudioBuffer.duration;\n\n // Create waveform data at 240 samples per second (pixel resolution)\n const samplesPerWaveformPoint = sampleRate / 240; // 240Hz\n const totalPoints = Math.floor(duration * 240);\n this.preCalculatedVocalsWaveform = new Uint8Array(totalPoints);\n\n for (let i = 0; i < duration * 240; i++) {\n const startSample = Math.floor(i * samplesPerWaveformPoint);\n const endSample = Math.min(Math.floor((i + 1) * samplesPerWaveformPoint), channelData.length);\n\n // Get peak value for this time segment (preserves waveform shape better than RMS)\n let maxVal = 0;\n let minVal = 0;\n for (let j = startSample; j < endSample; j++) {\n maxVal = Math.max(maxVal, channelData[j]);\n minVal = Math.min(minVal, channelData[j]);\n }\n\n // Use the larger absolute value to preserve the waveform peaks\n const peak = Math.max(Math.abs(maxVal), Math.abs(minVal));\n\n // Store as signed value: 128 is center, >128 is positive, <128 is negative\n // Determine sign based on which peak was larger\n const signedValue =\n Math.abs(maxVal) > Math.abs(minVal)\n ? 128 + Math.floor(peak * 127) // Positive peak\n : 128 - Math.floor(peak * 127); // Negative peak\n\n this.preCalculatedVocalsWaveform[i] = Math.max(0, Math.min(255, signedValue));\n }\n }\n\n updateWaveformData() {\n if (!this.analyser || !this.micDataArray) return;\n\n // Get time domain data from microphone (actual waveform)\n this.analyser.getByteTimeDomainData(this.micDataArray);\n\n // Calculate how many samples to shift based on time elapsed\n if (!this.lastMicTime) {\n this.lastMicTime = this.currentTime;\n }\n const timeElapsed = this.currentTime - this.lastMicTime;\n const samplesToShift = Math.floor(timeElapsed * 240);\n\n if (samplesToShift > 0) {\n // Shift array left by the number of samples elapsed\n for (let i = 0; i < 1440 - samplesToShift; i++) {\n this.waveformData[i] = this.waveformData[i + samplesToShift];\n }\n\n // Fill the right side with actual waveform data\n // Sample multiple points from the audio buffer to create smooth waveform\n const samplesPerPoint = Math.floor(this.micDataArray.length / samplesToShift);\n for (let i = 0; i < samplesToShift; i++) {\n const bufferIndex = Math.min(i * samplesPerPoint, this.micDataArray.length - 1);\n // Use actual waveform value (already 0-255)\n this.waveformData[1440 - samplesToShift + i] = this.micDataArray[bufferIndex];\n }\n\n this.lastMicTime = this.currentTime;\n }\n }\n\n updateVocalsWaveformData() {\n // Update vocals rendering array by slicing from source and padding with zeros\n if (this.preCalculatedVocalsWaveform) {\n const startIndex = Math.floor((this.currentTime - 6) * 240); // 6 seconds back\n const endIndex = startIndex + 1920; // 8 seconds total\n\n if (startIndex >= 0 && endIndex <= this.preCalculatedVocalsWaveform.length) {\n // Simple case: copy directly\n for (let i = 0; i < 1920; i++) {\n this.vocalsWaveformData[i] = this.preCalculatedVocalsWaveform[startIndex + i];\n }\n } else {\n // Edge cases: slice and concatenate with zero padding\n const validStart = Math.max(0, startIndex);\n const validEnd = Math.min(this.preCalculatedVocalsWaveform.length, endIndex);\n const leftPadding = validStart - startIndex;\n const rightPadding = endIndex - validEnd;\n\n let destIndex = 0;\n\n // Left padding (zeros)\n for (let i = 0; i < leftPadding; i++) {\n this.vocalsWaveformData[destIndex++] = 128;\n }\n\n // Valid source data\n for (let i = validStart; i < validEnd; i++) {\n this.vocalsWaveformData[destIndex++] = this.preCalculatedVocalsWaveform[i];\n }\n\n // Right padding (zeros)\n for (let i = 0; i < rightPadding; i++) {\n this.vocalsWaveformData[destIndex++] = 128;\n }\n }\n }\n }\n\n updateWaveformDataAtFixedRate() {\n const now = Date.now();\n\n // Initialize timing if needed\n if (!this.lastMicUpdateTime) {\n this.lastMicUpdateTime = now;\n this.micUpdateInterval = 1000 / 240; // 240Hz = 4.17ms intervals\n return;\n }\n\n // Only update if enough time has passed for exactly 60Hz\n if (now - this.lastMicUpdateTime >= this.micUpdateInterval) {\n this.updateWaveformData();\n this.updateVocalsWaveformData();\n this.lastMicUpdateTime = now;\n }\n }\n\n drawVocalsWaveform(width, height) {\n if (!this.isPlaying || !this.waveformPreferences.enableWaveforms) return;\n\n // If mic is disabled but waveforms are enabled, only show vocals\n if (!this.waveformPreferences.enableMic && this.waveformPreferences.enableWaveforms) {\n // Draw vocals waveform but show it where mic would be\n }\n\n const vocalsHeight = this.settings.vocalsWaveformHeight;\n const micHeight = this.settings.waveformHeight;\n const gap = this.settings.vocalsWaveformGap;\n const vocalsY = height - micHeight - gap - vocalsHeight - 20;\n const currentPositionX = width * this.settings.waveformCurrentPosition;\n\n // Direct pixel-to-data mapping for vocals (1920 pixels = 1920 data points)\n this.ctx.strokeStyle = this.settings.vocalsWaveformColor;\n this.ctx.lineWidth = 2;\n this.ctx.beginPath();\n\n const centerY = vocalsY + vocalsHeight / 2;\n let firstPoint = true;\n\n // Direct pixel-to-data mapping, left to right\n for (let x = 0; x < width; x++) {\n // Convert byte data (0-255) to waveform position (-1 to 1), centered at 128\n const normalized = (this.vocalsWaveformData[x] - 128) / 128;\n const y = centerY + normalized * vocalsHeight * 1.5; // Increased amplitude\n\n if (firstPoint) {\n this.ctx.moveTo(x, y);\n firstPoint = false;\n } else {\n this.ctx.lineTo(x, y);\n }\n }\n\n this.ctx.stroke();\n\n // Draw current position indicator\n this.ctx.strokeStyle = '#ffffff';\n this.ctx.lineWidth = 1;\n this.ctx.beginPath();\n this.ctx.moveTo(currentPositionX, vocalsY);\n this.ctx.lineTo(currentPositionX, vocalsY + vocalsHeight);\n this.ctx.stroke();\n\n // Draw center line\n this.ctx.strokeStyle = '#666666';\n this.ctx.lineWidth = 1;\n this.ctx.beginPath();\n this.ctx.moveTo(0, centerY);\n this.ctx.lineTo(width, centerY);\n this.ctx.stroke();\n }\n\n // Control methods for preferences\n setWaveformsEnabled(enabled) {\n this.waveformPreferences.enableWaveforms = enabled;\n }\n\n setMicToSpeakers(enabled) {\n this.waveformPreferences.micToSpeakers = enabled;\n\n // karaokeRenderer mic is ONLY for waveform visualization, NOT audio routing\n // kaiPlayer handles all microphone-to-speaker routing to PA/IEM outputs\n // This setting is stored for preferences sync but not used by karaokeRenderer\n }\n\n setMicEnabled(enabled) {\n this.waveformPreferences.enableMic = enabled;\n\n if (enabled && this.isPlaying) {\n this.startMicrophoneCapture();\n } else if (!enabled) {\n this.stopMicrophoneCapture();\n // Clear the waveform buffer so we don't render a flatline\n this.waveformData.fill(128); // Fill with center value (128 = silence)\n }\n }\n\n setEffectsEnabled(enabled) {\n this.waveformPreferences.enableEffects = enabled;\n\n // Clear effects canvas when disabled\n if (!enabled && this.effectsGL) {\n this.effectsGL.clearColor(0, 0, 0, 1);\n this.effectsGL.clear(this.effectsGL.COLOR_BUFFER_BIT);\n }\n }\n\n drawMicrophoneWaveform(width, height) {\n if (!this.isPlaying || !this.waveformPreferences.enableWaveforms) return;\n\n // Only draw mic waveform if mic is enabled AND we have actual mic data\n if (!this.waveformPreferences.enableMic || !this.micStream || !this.analyser) return;\n\n const waveformHeight = this.settings.waveformHeight;\n const waveformY = height - waveformHeight - 20;\n const currentPositionX = width * this.settings.waveformCurrentPosition;\n\n // Direct pixel-to-data mapping for mic (1440 pixels for 6 seconds, scaled to 1920 width)\n this.ctx.strokeStyle = this.settings.waveformColor;\n this.ctx.lineWidth = 2;\n this.ctx.beginPath();\n\n const centerY = waveformY + waveformHeight / 2;\n let firstPoint = true;\n\n // Direct pixel-to-data mapping for mic, left to right\n for (let x = 0; x < 1440; x++) {\n // Convert byte data (0-255) to waveform position (-1 to 1)\n const normalized = (this.waveformData[x] - 128) / 128;\n const y = centerY + normalized * waveformHeight * 1.5; // Increased amplitude\n\n if (firstPoint) {\n this.ctx.moveTo(x, y);\n firstPoint = false;\n } else {\n this.ctx.lineTo(x, y);\n }\n }\n\n this.ctx.stroke();\n\n // Draw current position indicator\n this.ctx.strokeStyle = '#ffffff';\n this.ctx.lineWidth = 1;\n this.ctx.beginPath();\n this.ctx.moveTo(currentPositionX, waveformY);\n this.ctx.lineTo(currentPositionX, waveformY + waveformHeight);\n this.ctx.stroke();\n\n // Draw center line\n this.ctx.strokeStyle = '#666666';\n this.ctx.lineWidth = 1;\n this.ctx.beginPath();\n this.ctx.moveTo(0, centerY);\n this.ctx.lineTo(width, centerY);\n this.ctx.stroke();\n }\n\n startAnimation() {\n const animate = (currentTime) => {\n // Track actual FPS by measuring time between frames\n const deltaTime = currentTime - this.lastFrameTime;\n this.lastFrameTime = currentTime;\n\n // Add to FPS history (keep last 60 samples for 1-second average)\n this.fpsHistory.push(1000 / deltaTime);\n if (this.fpsHistory.length > 60) {\n this.fpsHistory.shift();\n }\n\n // Time the full frame including updates\n const frameStart = performance.now();\n\n this.draw();\n\n // Track time spent in updates vs rendering\n this.frameUpdateTime = performance.now() - frameStart;\n\n this.animationFrame = requestAnimationFrame(animate);\n };\n this.animationFrame = requestAnimationFrame(animate);\n }\n\n stopAnimation() {\n if (this.animationFrame) {\n cancelAnimationFrame(this.animationFrame);\n this.animationFrame = null;\n }\n }\n\n draw() {\n const width = this.canvas.width; // Always 1920\n const height = this.canvas.height; // Always 1080\n\n this.frameCount++;\n\n // Performance profiling - sample every 2 seconds\n const shouldProfile = this.frameCount % 120 === 0;\n const frameStart = shouldProfile ? performance.now() : 0;\n\n // Update microphone waveform data at consistent 60Hz rate\n this.updateWaveformDataAtFixedRate();\n\n // Clear canvas with dark background\n this.ctx.fillStyle = '#000000';\n this.ctx.fillRect(0, 0, width, height);\n\n // Render WebGL effects to offscreen canvas\n const effectsStart = shouldProfile ? performance.now() : 0;\n this.renderWebGLEffects();\n const effectsEnd = shouldProfile ? performance.now() : 0;\n\n // Composite WebGL effects onto main canvas at full opacity\n if (this.effectsCanvas) {\n this.ctx.save();\n this.ctx.globalAlpha = 1.0;\n this.ctx.drawImage(this.effectsCanvas, 0, 0);\n this.ctx.restore();\n }\n\n // Add dark overlay for text contrast (let effects show through)\n this.ctx.save();\n this.ctx.globalAlpha = this.waveformPreferences.overlayOpacity; // Configurable opacity dark overlay for better text readability\n this.ctx.fillStyle = '#000000';\n this.ctx.fillRect(0, 0, width, height);\n this.ctx.restore();\n\n // Draw waveforms at the bottom\n const waveformsStart = shouldProfile ? performance.now() : 0;\n this.drawVocalsWaveform(width, height);\n const vocalsEnd = shouldProfile ? performance.now() : 0;\n this.drawMicrophoneWaveform(width, height);\n const micEnd = shouldProfile ? performance.now() : 0;\n\n // Show song info when loaded but not playing\n if (!this.isPlaying && this.songMetadata) {\n this.drawSongInfo(width, height, this.songMetadata);\n // Draw QR code AFTER song info\n this.drawQRCodeOverlay(width, height);\n // Draw queue display AFTER QR code\n this.drawQueueDisplay(width, height);\n return;\n }\n\n if (!this.lyrics || this.lyrics.length === 0) {\n // Draw QR code when no lyrics\n this.drawQRCodeOverlay(width, height);\n // Draw queue display AFTER QR code\n this.drawQueueDisplay(width, height);\n return;\n }\n\n // Check for instrumental intro first\n if (this.isInInstrumentalIntro()) {\n this.drawInstrumentalIntro(width, height);\n // QR code not shown during playback\n return;\n }\n\n // Check for instrumental outro (just show clean ending, no progress bar)\n if (this.isInInstrumentalOutro()) {\n this.drawInstrumentalOutro(width, height);\n // QR code not shown during playback\n return;\n }\n\n // Find current line\n const currentLineIndex = this.findCurrentLine();\n\n // Check for singer change (for backup:PA feature)\n this.checkSingerChange(currentLineIndex);\n\n if (currentLineIndex >= 0 && currentLineIndex < this.lyrics.length) {\n // Check if we're in an instrumental gap first\n const isInInstrumentalGap = this.isInInstrumentalGap(currentLineIndex);\n\n if (isInInstrumentalGap) {\n // During instrumental sections, only show the progress bar and upcoming lyrics\n this.drawInstrumentalProgressBar(currentLineIndex, width, height);\n } else {\n // Normal lyric display - show all active lines (main + backup)\n this.drawActiveLines(width, height);\n }\n } else {\n // No current main line found - check if we should show progress bar during backup-only periods\n this.drawBackupOnlyProgressBar(width, height);\n }\n\n // Performance profiling output with real FPS tracking\n if (shouldProfile) {\n const frameEnd = performance.now();\n const _clearTime = effectsStart - frameStart;\n const _effectsTime = effectsEnd - effectsStart;\n const _vocalsTime = vocalsEnd - waveformsStart;\n const _micTime = micEnd - vocalsEnd;\n const _lyricsTime = frameEnd - micEnd;\n const _renderTime = frameEnd - frameStart;\n\n // Calculate actual FPS average\n const _avgFPS =\n this.fpsHistory.length > 0\n ? this.fpsHistory.reduce((a, b) => a + b, 0) / this.fpsHistory.length\n : 0;\n\n // Calculate frame budget utilization\n const targetFrameTime = 1000 / 60; // 16.67ms for 60fps\n const _budgetUsed = (this.frameUpdateTime / targetFrameTime) * 100;\n\n // Log as separate lines to avoid console truncation\n }\n }\n\n /**\n * Draw QR code in bottom left corner (only when not playing)\n */\n drawQRCodeOverlay(width, height) {\n // Only show when not playing\n if (!this.showQrCode || !this.qrCodeCanvas || this.isPlaying) {\n return;\n }\n\n const padding = 20;\n const qrSize = 150;\n const x = padding; // Bottom left instead of right\n const y = height - qrSize - padding;\n\n // Draw white background with shadow\n this.ctx.save();\n this.ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';\n this.ctx.shadowBlur = 10;\n this.ctx.shadowOffsetX = 2;\n this.ctx.shadowOffsetY = 2;\n this.ctx.fillStyle = '#FFFFFF';\n this.ctx.fillRect(x - 10, y - 10, qrSize + 20, qrSize + 20);\n this.ctx.restore();\n\n // Draw QR code\n this.ctx.drawImage(this.qrCodeCanvas, x, y, qrSize, qrSize);\n }\n\n /**\n * Draw queue display in bottom right corner (only when not playing)\n */\n drawQueueDisplay(width, height) {\n // Only show when setting is enabled and queue has items\n if (!this.displayQueue || !this.queueItems || this.queueItems.length === 0 || this.isPlaying) {\n return;\n }\n\n const padding = 120; // Move further from edge (left)\n const bottomPadding = 80; // Move up from bottom\n const rightX = width - padding;\n const lineHeight = 64;\n const labelFontSize = 48;\n const songFontSize = 40;\n\n this.ctx.save();\n\n // Calculate text dimensions for background\n this.ctx.font = `bold ${labelFontSize}px sans-serif`;\n const labelText = 'Next up:';\n const labelWidth = this.ctx.measureText(labelText).width;\n\n // Measure all song texts and prepare data\n let maxWidth = labelWidth;\n const songData = this.queueItems.slice(0, 3).map((item) => {\n const title = item.title || item.song?.title || 'Unknown';\n const singer = item.requester || item.singer || '';\n\n // Measure title\n this.ctx.font = `${songFontSize}px sans-serif`;\n const titleWidth = this.ctx.measureText(title).width;\n\n // Measure singer if present\n let singerWidth = 0;\n if (singer) {\n const singerText = ` - ${singer}`;\n singerWidth = this.ctx.measureText(singerText).width;\n }\n\n const totalWidth = titleWidth + singerWidth;\n maxWidth = Math.max(maxWidth, totalWidth);\n\n return { title, singer };\n });\n\n // Calculate background dimensions\n const bgWidth = maxWidth + 30;\n const bgHeight = lineHeight + songData.length * lineHeight + 20;\n const bgX = rightX - bgWidth;\n const bgY = height - bgHeight - bottomPadding;\n\n // Draw semi-transparent background with shadow and rounded corners\n this.ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';\n this.ctx.shadowBlur = 10;\n this.ctx.shadowOffsetX = 2;\n this.ctx.shadowOffsetY = 2;\n this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';\n\n // Draw rounded rectangle\n const radius = 10;\n this.ctx.beginPath();\n this.ctx.moveTo(bgX + radius, bgY);\n this.ctx.lineTo(bgX + bgWidth - radius, bgY);\n this.ctx.quadraticCurveTo(bgX + bgWidth, bgY, bgX + bgWidth, bgY + radius);\n this.ctx.lineTo(bgX + bgWidth, bgY + bgHeight - radius);\n this.ctx.quadraticCurveTo(\n bgX + bgWidth,\n bgY + bgHeight,\n bgX + bgWidth - radius,\n bgY + bgHeight\n );\n this.ctx.lineTo(bgX + radius, bgY + bgHeight);\n this.ctx.quadraticCurveTo(bgX, bgY + bgHeight, bgX, bgY + bgHeight - radius);\n this.ctx.lineTo(bgX, bgY + radius);\n this.ctx.quadraticCurveTo(bgX, bgY, bgX + radius, bgY);\n this.ctx.closePath();\n this.ctx.fill();\n\n this.ctx.shadowColor = 'transparent';\n\n // Draw \"Next up:\" label in blue\n this.ctx.font = `bold ${labelFontSize}px sans-serif`;\n this.ctx.fillStyle = '#3B82F6'; // Tailwind blue-600\n this.ctx.textAlign = 'left';\n this.ctx.fillText(labelText, bgX + 15, bgY + labelFontSize + 10);\n\n // Draw queue items\n this.ctx.font = `${songFontSize}px sans-serif`;\n songData.forEach((item, index) => {\n const textY = bgY + labelFontSize + 10 + (index + 1) * lineHeight;\n const textX = bgX + 15;\n\n // Draw title in white\n this.ctx.fillStyle = '#FFFFFF';\n this.ctx.fillText(item.title, textX, textY);\n\n // Draw singer in yellow if present and not \"KJ\"\n if (item.singer) {\n const titleWidth = this.ctx.measureText(item.title).width;\n const isKJ = item.singer.toUpperCase() === 'KJ';\n this.ctx.fillStyle = isKJ ? '#FFFFFF' : '#FCD34D'; // yellow-300 for non-KJ singers\n this.ctx.fillText(` - ${item.singer}`, textX + titleWidth, textY);\n }\n });\n\n this.ctx.restore();\n }\n\n findCurrentLine() {\n // Use cached result if time hasn't changed much\n if (\n Math.abs(this.currentTime - this.lastTimeForLineCalculation) < this.lineCalculationTolerance\n ) {\n return this.cachedCurrentLine;\n }\n\n // Find current main singer line (exclude backup singers for progress tracking)\n this.cachedCurrentLine = this.findCurrentMainLine();\n this.lastTimeForLineCalculation = this.currentTime;\n return this.cachedCurrentLine;\n }\n\n findCurrentMainLine() {\n if (!this.lyrics) return -1;\n\n for (let i = 0; i < this.lyrics.length; i++) {\n const line = this.lyrics[i];\n // Only consider main singer lines for progress tracking\n if (\n !line.isBackup &&\n this.currentTime >= line.startTime &&\n this.currentTime <= line.endTime\n ) {\n return i;\n }\n }\n\n // Find the closest upcoming main singer line\n for (let i = 0; i < this.lyrics.length; i++) {\n if (!this.lyrics[i].isBackup && this.currentTime < this.lyrics[i].startTime) {\n // Find the previous main singer line (not backup)\n for (let j = i - 1; j >= 0; j--) {\n if (!this.lyrics[j].isBackup) {\n return j;\n }\n }\n // No previous main singer line found, return -1 to trigger progress bar\n return -1;\n }\n }\n\n // Find the last main singer line\n for (let i = this.lyrics.length - 1; i >= 0; i--) {\n if (!this.lyrics[i].isBackup) {\n return i;\n }\n }\n\n return -1;\n }\n\n /**\n * Check if the current active line's singer has changed and notify via callback.\n * This is used for the backup:PA feature to route vocals to PA when needed.\n */\n checkSingerChange() {\n if (!this.onSingerChange || !this.lyrics) return;\n\n // Find ANY line we're currently in (including backup lines)\n // This is different from findCurrentMainLine which excludes backup\n let currentSinger = null;\n for (let i = 0; i < this.lyrics.length; i++) {\n const line = this.lyrics[i];\n if (this.currentTime >= line.startTime && this.currentTime <= line.endTime) {\n if (line.singer) {\n currentSinger = line.singer;\n break; // Use first matching line with singer\n }\n }\n }\n\n // Trigger callback when singer changes\n if (currentSinger !== this.lastActiveSinger) {\n this.lastActiveSinger = currentSinger;\n this.onSingerChange(currentSinger);\n }\n }\n\n drawCurrentLyricLine(currentLineIndex, canvasWidth, canvasHeight) {\n if (currentLineIndex < 0 || currentLineIndex >= this.lyrics.length) return;\n\n const line = this.lyrics[currentLineIndex];\n\n // Set up font\n this.ctx.font = `${this.settings.fontSize}px ${this.settings.fontFamily}`;\n this.ctx.textAlign = 'center';\n this.ctx.fillStyle = this.settings.activeColor; // Light blue for current line\n\n // Get text from line (KAI format may have different text fields)\n let text = '';\n if (line.text) {\n text = line.text;\n } else if (line.words && line.words.length > 0) {\n // If we have words array, join them\n text = line.words.map((w) => w.text || w.word || w).join(' ');\n }\n\n if (text && text.trim() !== '') {\n // Handle long text with proper wrapping\n const maxWidth = canvasWidth * 0.9;\n const words = text.split(' ');\n const lines = [];\n let currentLine = '';\n\n for (const word of words) {\n const testLine = currentLine ? currentLine + ' ' + word : word;\n const testWidth = this.ctx.measureText(testLine).width;\n\n if (testWidth <= maxWidth) {\n currentLine = testLine;\n } else {\n if (currentLine) {\n lines.push(currentLine);\n currentLine = word;\n } else {\n // Single word is too long, just add it anyway\n lines.push(word);\n }\n }\n }\n\n if (currentLine) {\n lines.push(currentLine);\n }\n\n // Draw each line centered vertically\n const lineSpacing = this.settings.lineHeight * 0.9;\n const totalHeight = lines.length * lineSpacing;\n let currentY = canvasHeight / 2 - totalHeight / 2 + lineSpacing;\n\n lines.forEach((line) => {\n this.drawTextWithBackground(line, canvasWidth / 2, currentY);\n currentY += lineSpacing;\n });\n }\n }\n\n drawActiveLines(canvasWidth, canvasHeight, skipUpcoming = false) {\n if (!this.lyrics) return;\n\n // Update backup singer animations first\n this.updateBackupAnimations();\n\n // Find all active lines at current time (both main and backup singers)\n const activeLines = [];\n // Use interpolated time for precise 60fps lyric timing\n const now = this.getInterpolatedTime();\n\n for (let i = 0; i < this.lyrics.length; i++) {\n const line = this.lyrics[i];\n\n // Skip lines that are currently transitioning (prevents double rendering)\n if (this.lyricTransitions.has(i)) {\n continue;\n }\n\n // Skip lines that are hidden during transitions (prevents overlap)\n if (this.hiddenDuringTransition.has(i)) {\n continue;\n }\n\n if (!line.isDisabled && now >= line.startTime && now <= line.endTime) {\n activeLines.push({ ...line, index: i });\n }\n }\n\n // Separate main and backup singers\n const mainLines = activeLines.filter((line) => !line.isBackup);\n const backupLines = activeLines.filter((line) => line.isBackup);\n\n // Calculate vertical positioning for main lines only (backup renders at bottom separately)\n const totalMainLines = Math.max(1, mainLines.length);\n const lineSpacing = this.settings.lineHeight * 1.2;\n const totalHeight = totalMainLines * lineSpacing;\n let currentY = canvasHeight / 2 - totalHeight / 2 + lineSpacing - 180; // Move up by 180 pixels for more room below\n\n // Draw main singer lines\n mainLines.forEach((line) => {\n const nextY = this.drawSingleLine(line, canvasWidth, currentY, false); // false = main singer\n currentY = nextY || currentY + lineSpacing; // Use returned Y or fallback to old spacing\n });\n\n // Draw backup singer lines at bottom of screen (fixed position)\n // This keeps them out of the way and makes them feel less jarring\n if (backupLines.length > 0) {\n const bottomPadding = 10; // Distance from bottom of screen\n\n // Pre-calculate total height needed for backup lines (accounting for text wrapping)\n const maxWidth = canvasWidth * 0.9;\n let totalBackupHeight = 0;\n\n // Set font for measurement (italic for backup)\n this.ctx.font = `italic ${this.settings.fontSize}px ${this.settings.fontFamily}`;\n\n backupLines.forEach((line) => {\n const text = line.text || '';\n const words = text.split(' ');\n let wrappedLineCount = 1;\n let currentLine = '';\n\n for (const word of words) {\n const testLine = currentLine ? currentLine + ' ' + word : word;\n const testWidth = this.ctx.measureText(testLine).width;\n\n if (testWidth <= maxWidth) {\n currentLine = testLine;\n } else {\n if (currentLine) {\n wrappedLineCount++;\n currentLine = word;\n }\n }\n }\n\n totalBackupHeight += wrappedLineCount * this.settings.lineHeight * 0.8;\n });\n\n // Position so bottom of backup content is at bottomPadding from screen bottom\n let backupY = canvasHeight - bottomPadding - totalBackupHeight;\n\n backupLines.forEach((line) => {\n const animation = this.backupAnimations.get(line.index);\n const alpha = animation ? animation.alpha : this.settings.backupMaxAlpha;\n const nextY = this.drawSingleLine(line, canvasWidth, backupY, true, alpha); // true = backup singer\n backupY = nextY || backupY + this.settings.lineHeight * 0.8;\n });\n }\n\n // Save the bottom position after drawing active lyrics (only if there were active lyrics)\n const hasActiveLyrics = activeLines.length > 0;\n if (hasActiveLyrics) {\n this.lastActiveLyricsBottom = currentY;\n }\n\n // Calculate upcoming position for both animations and drawing\n // If there are transitioning lyrics but no active lyrics, estimate position based on transitions\n let upcomingY = (this.lastActiveLyricsBottom || currentY) + 10;\n\n // If we have transitioning lyrics but no active lyrics (e.g., intro just ended),\n // position upcoming below the transitioning lyric's current position\n if (!hasActiveLyrics && this.lyricTransitions.size > 0) {\n // Find the lowest transitioning lyric position\n let lowestTransitionY = 0;\n for (const [_lineIndex, transition] of this.lyricTransitions.entries()) {\n const transitionY =\n transition.startY + (transition.endY - transition.startY) * transition.progress;\n lowestTransitionY = Math.max(lowestTransitionY, transitionY);\n }\n if (lowestTransitionY > 0) {\n upcomingY = lowestTransitionY + this.settings.lineHeight * 1.5; // Position below with spacing\n }\n }\n\n // Check for lyrics transitioning from upcoming to active and start animations\n this.startTransitionAnimations(activeLines, upcomingY);\n\n // Draw transitioning lyrics (animating from upcoming to active)\n for (const [lineIndex, transition] of this.lyricTransitions.entries()) {\n const lyricLine = this.lyrics[lineIndex];\n if (lyricLine) {\n this.drawTransitioningLine(lyricLine, canvasWidth, transition);\n }\n }\n\n // Draw upcoming lyrics if enabled and not skipped (positioned dynamically after current lyrics)\n // Show upcoming lyrics when:\n // 1. Not currently skipping upcoming display\n // 2. Setting is enabled\n // 3. No active transitions (prevents flash during transitions)\n // The upcoming lyric will show in gray below the current lyric and animate up when it becomes active\n if (\n !skipUpcoming &&\n this.waveformPreferences.showUpcomingLyrics &&\n this.lyricTransitions.size === 0\n ) {\n this.drawUpcomingLyrics(canvasWidth, canvasHeight, upcomingY);\n }\n }\n\n /**\n * Get the display color for a line based on its singer type.\n * @param {object} line - The lyric line with optional singer field\n * @returns {string} - The hex color to use for the line\n */\n getSingerColor(line) {\n const singer = line.singer;\n if (!singer) return this.settings.activeColor; // Default lead (A)\n\n if (singer === 'B') return this.settings.singerBColor;\n if (singer === 'duet') return this.settings.duetColor;\n if (singer === 'backup:PA') return this.settings.backupPAColor;\n if (singer === 'backup') return this.settings.backupActiveColor;\n\n // For any other value (like 'A'), use default active color\n return this.settings.activeColor;\n }\n\n /**\n * Get the prefix icon for a singer type.\n * @param {object} line - The lyric line with optional singer field\n * @returns {string} - The prefix string (icon or empty)\n */\n getSingerPrefix(line) {\n const singer = line.singer;\n // backup:PA uses brighter color instead of prefix (audio is the indicator)\n if (singer === 'backup') return '♪ ';\n return '';\n }\n\n drawSingleLine(line, canvasWidth, yPosition, isBackup, alpha = 1.0) {\n // Set up font (italic for backup singers)\n if (isBackup) {\n this.ctx.font = `italic ${this.settings.fontSize}px ${this.settings.fontFamily}`;\n } else {\n this.ctx.font = `${this.settings.fontSize}px ${this.settings.fontFamily}`;\n }\n this.ctx.textAlign = 'center';\n\n // Save context for alpha manipulation\n this.ctx.save();\n\n // Apply alpha for backup singers\n if (isBackup) {\n this.ctx.globalAlpha = alpha;\n }\n\n // Choose colors based on singer type (uses new singer field if available)\n this.ctx.fillStyle = this.getSingerColor(line);\n\n // Get text from line\n let text = '';\n if (line.text) {\n text = line.text;\n } else if (line.words && line.words.length > 0) {\n text = line.words.map((w) => w.text || w.word || w).join(' ');\n }\n\n if (text && text.trim() !== '') {\n // Handle long text with proper wrapping\n const maxWidth = canvasWidth * 0.9;\n const words = text.split(' ');\n const lines = [];\n let currentLine = '';\n\n for (const word of words) {\n const testLine = currentLine ? currentLine + ' ' + word : word;\n const testWidth = this.ctx.measureText(testLine).width;\n\n if (testWidth <= maxWidth) {\n currentLine = testLine;\n } else {\n if (currentLine) {\n lines.push(currentLine);\n currentLine = word;\n } else {\n lines.push(word);\n }\n }\n }\n\n if (currentLine) {\n lines.push(currentLine);\n }\n\n // Draw each wrapped line\n let finalY = yPosition;\n const prefix = this.getSingerPrefix(line);\n // Get glow color for non-lead singers (helps identify whose line it is)\n const glowColor = line.singer ? this.getSingerColor(line) : null;\n lines.forEach((textLine, index) => {\n const adjustedY = yPosition + index * this.settings.lineHeight * 0.8;\n finalY = adjustedY + this.settings.lineHeight * 0.8; // Bottom of this line\n\n // Add visual indicator prefix for first line only (based on singer type)\n if (index === 0 && prefix) {\n const prefixedText = `${prefix}${textLine}`;\n this.drawTextWithBackground(prefixedText, canvasWidth / 2, adjustedY, glowColor);\n } else {\n this.drawTextWithBackground(textLine, canvasWidth / 2, adjustedY, glowColor);\n }\n });\n\n // Restore context (removes alpha changes)\n this.ctx.restore();\n\n // Return the bottom Y position after all wrapped lines\n return finalY;\n }\n\n // Restore context (removes alpha changes)\n this.ctx.restore();\n\n // Return null if no text was drawn (fallback to old spacing)\n return null;\n }\n\n updateBackupAnimations() {\n if (!this.lyrics) return;\n\n // Use interpolated time for smooth 60fps backup singer fade animations\n const now = this.getInterpolatedTime();\n const _frameDelta = 16; // Assuming 60fps (16ms per frame)\n\n for (let i = 0; i < this.lyrics.length; i++) {\n const line = this.lyrics[i];\n\n // Skip non-backup or disabled lines\n if (!line.isBackup || line.isDisabled) {\n this.backupAnimations.delete(i);\n continue;\n }\n\n const isActive = now >= line.startTime && now <= line.endTime;\n const animation = this.backupAnimations.get(i) || {\n alpha: this.settings.backupMinAlpha,\n fadeDirection: 0, // 0 = stable, 1 = fading in, -1 = fading out\n lastStateChange: now,\n };\n\n // Determine if we need to change fade direction\n const targetAlpha = isActive ? this.settings.backupMaxAlpha : this.settings.backupMinAlpha;\n let newFadeDirection = 0;\n\n if (isActive && animation.alpha < this.settings.backupMaxAlpha) {\n newFadeDirection = 1; // Fade in\n } else if (!isActive && animation.alpha > this.settings.backupMinAlpha) {\n newFadeDirection = -1; // Fade out\n }\n\n // Update fade direction if it changed\n if (newFadeDirection !== animation.fadeDirection) {\n animation.fadeDirection = newFadeDirection;\n animation.lastStateChange = now;\n }\n\n // Calculate alpha based on fade direction\n if (animation.fadeDirection !== 0) {\n const elapsed = now - animation.lastStateChange;\n const progress = Math.min(elapsed / this.settings.backupFadeDuration, 1.0);\n\n // Apply easing (simple ease-out)\n const easedProgress = 1 - Math.pow(1 - progress, 3);\n\n if (animation.fadeDirection === 1) {\n // Fading in\n animation.alpha =\n this.settings.backupMinAlpha +\n (this.settings.backupMaxAlpha - this.settings.backupMinAlpha) * easedProgress;\n } else {\n // Fading out\n animation.alpha =\n this.settings.backupMaxAlpha -\n (this.settings.backupMaxAlpha - this.settings.backupMinAlpha) * easedProgress;\n }\n\n // Stop fading when complete\n if (progress >= 1.0) {\n animation.fadeDirection = 0;\n animation.alpha = targetAlpha;\n }\n }\n\n // Store the updated animation\n this.backupAnimations.set(i, animation);\n }\n\n // Clean up animations for lines that no longer exist\n for (const [lineIndex] of this.backupAnimations) {\n if (lineIndex >= this.lyrics.length) {\n this.backupAnimations.delete(lineIndex);\n }\n }\n }\n\n wrapWordsToLines(words, maxWidth) {\n const lines = [];\n let currentLine = [];\n let currentWidth = 0;\n\n words.forEach((word, index) => {\n const wordWidth = this.ctx.measureText(word.text).width;\n const spaceWidth = index > 0 ? this.settings.wordSpacing : 0;\n const totalWidth = currentWidth + spaceWidth + wordWidth;\n\n if (totalWidth <= maxWidth || currentLine.length === 0) {\n // Add word to current line\n currentLine.push(word);\n currentWidth = totalWidth;\n } else {\n // Start new line\n if (currentLine.length > 0) {\n lines.push(currentLine);\n }\n currentLine = [word];\n currentWidth = wordWidth;\n }\n });\n\n if (currentLine.length > 0) {\n lines.push(currentLine);\n }\n\n return lines;\n }\n\n drawWordLine(words, centerX, y, maxWidth, isCurrentLine) {\n // Calculate total width of this line\n const totalWidth = words.reduce((width, word, index) => {\n const wordWidth = this.ctx.measureText(word.text).width;\n const spacing = index < words.length - 1 ? this.settings.wordSpacing : 0;\n return width + wordWidth + spacing;\n }, 0);\n\n // Start position for centering\n let x = centerX - totalWidth / 2;\n\n words.forEach((word, _index) => {\n const isActiveWord =\n isCurrentLine && this.currentTime >= word.startTime && this.currentTime <= word.endTime;\n\n // Set color\n this.ctx.fillStyle = isActiveWord\n ? this.settings.activeColor\n : isCurrentLine\n ? '#CCCCCC'\n : this.settings.textColor;\n\n // Draw word\n this.ctx.textAlign = 'left';\n this.drawTextWithBackground(word.text, x, y);\n\n // Draw bouncing ball for active word\n if (isActiveWord && isCurrentLine) {\n this.drawBouncingBall(x, word, y);\n }\n\n // Move to next word position\n const wordWidth = this.ctx.measureText(word.text).width;\n x += wordWidth + this.settings.wordSpacing;\n });\n }\n\n isInInstrumentalIntro() {\n if (!this.lyrics || this.lyrics.length === 0) return false;\n\n const now = this.getInterpolatedTime();\n const firstLine = this.lyrics[0];\n\n if (!firstLine) return false;\n\n // Check if we're before the first lyric starts\n return now < firstLine.startTime;\n }\n\n isInInstrumentalOutro() {\n if (!this.lyrics || this.lyrics.length === 0 || !this.songDuration) return false;\n\n const now = this.getInterpolatedTime();\n // Find the last enabled main singer line (not backup, not disabled)\n let lastMainLine = null;\n for (let i = this.lyrics.length - 1; i >= 0; i--) {\n const line = this.lyrics[i];\n if (!line.isBackup && !line.isDisabled) {\n lastMainLine = line;\n break;\n }\n }\n\n if (!lastMainLine) return false;\n\n // Check if we're after the last main singer line and there's enough outro time\n const outroLength = this.songDuration - lastMainLine.endTime;\n return now > lastMainLine.endTime && outroLength > 0;\n }\n\n getLastMainSingerLine() {\n if (!this.lyrics) return null;\n\n // Find the last enabled main singer line (not backup, not disabled)\n for (let i = this.lyrics.length - 1; i >= 0; i--) {\n const line = this.lyrics[i];\n if (!line.isBackup && !line.isDisabled) {\n return line;\n }\n }\n return null;\n }\n\n isInInstrumentalGap(currentLineIndex) {\n if (!this.lyrics || currentLineIndex < 0) return false;\n\n const now = this.getInterpolatedTime();\n const currentLine = this.lyrics[currentLineIndex];\n\n // Find the NEXT MAIN SINGER line (skip backup singers)\n let nextMainLine = null;\n for (let i = currentLineIndex + 1; i < this.lyrics.length; i++) {\n if (!this.lyrics[i].isBackup && !this.lyrics[i].isDisabled) {\n nextMainLine = this.lyrics[i];\n break;\n }\n }\n\n if (!currentLine || !nextMainLine) return false;\n\n const currentLineEnd = currentLine.endTime;\n const nextLineStart = nextMainLine.startTime;\n const gapDuration = nextLineStart - currentLineEnd;\n\n // Only consider it an instrumental gap if it's longer than 5 seconds\n if (gapDuration <= 5) return false;\n\n // Check if we're currently in the gap between main singers\n return now >= currentLineEnd && now <= nextLineStart;\n }\n\n isInMainSingerInstrumentalGap() {\n if (!this.lyrics) return { isInGap: false };\n\n const now = this.getInterpolatedTime();\n\n // Find the last main singer line that has ended\n let lastMainLine = null;\n let lastMainLineIndex = -1;\n for (let i = 0; i < this.lyrics.length; i++) {\n const line = this.lyrics[i];\n if (!line.isBackup && !line.isDisabled && now >= line.endTime) {\n lastMainLine = line;\n lastMainLineIndex = i;\n }\n }\n\n if (!lastMainLine) return { isInGap: false };\n\n // Find the next main singer line that hasn't started yet\n let nextMainLine = null;\n for (let i = lastMainLineIndex + 1; i < this.lyrics.length; i++) {\n const line = this.lyrics[i];\n if (!line.isBackup && !line.isDisabled && now < line.startTime) {\n nextMainLine = line;\n break;\n }\n }\n\n if (!nextMainLine) return { isInGap: false };\n\n const gapDuration = nextMainLine.startTime - lastMainLine.endTime;\n\n // Only consider it an instrumental gap if it's longer than 5 seconds\n if (gapDuration <= 5) return { isInGap: false };\n\n // Check if we're currently in the gap between main singers\n const isInGap = now >= lastMainLine.endTime && now <= nextMainLine.startTime;\n\n return {\n isInGap,\n lastMainLineIndex,\n nextMainLine,\n gapProgress: isInGap ? (now - lastMainLine.endTime) / gapDuration : 0,\n };\n }\n\n drawInstrumentalProgressBar(currentLineIndex, canvasWidth, canvasHeight) {\n if (!this.lyrics || currentLineIndex < 0) return;\n\n // Use interpolated time for smooth 60fps progress bar\n const now = this.getInterpolatedTime();\n const currentLine = this.lyrics[currentLineIndex];\n\n // Find the next main singer line (skip backup singers and disabled lines)\n let nextMainLine = null;\n // let nextMainLineIndex = -1; // Reserved for future use\n for (let i = currentLineIndex + 1; i < this.lyrics.length; i++) {\n if (!this.lyrics[i].isBackup && !this.lyrics[i].isDisabled) {\n nextMainLine = this.lyrics[i];\n // nextMainLineIndex = i;\n break;\n }\n }\n\n if (!currentLine || !nextMainLine) return;\n\n // Check if we're in an instrumental section (between current line end and next main line start)\n const currentLineEnd = currentLine.endTime;\n const nextLineStart = nextMainLine.startTime;\n const gapDuration = nextLineStart - currentLineEnd;\n\n // Only show progress bar for instrumental gaps longer than 5 seconds\n if (gapDuration <= 5) return;\n\n // Are we currently in the instrumental gap?\n if (now >= currentLineEnd && now <= nextLineStart) {\n // We're in the instrumental section - show progress bar and upcoming lyrics\n const gapProgress = (now - currentLineEnd) / gapDuration;\n const _timeRemaining = nextLineStart - now;\n\n // Draw progress bar at top\n const barWidth = canvasWidth * 0.8;\n const barX = (canvasWidth - barWidth) / 2;\n const barY = 80;\n\n const _barInfo = this.drawProgressBar(\n barX,\n barY,\n barWidth,\n undefined,\n gapProgress,\n canvasWidth\n );\n\n // Draw upcoming lyrics preview below progress bar with proper spacing\n this.drawUpcomingLyricsPreview(\n nextMainLine,\n canvasWidth,\n canvasHeight,\n gapProgress,\n barY + this.settings.progressBarMargin\n );\n\n // Draw any active backup singers during the instrumental gap (but skip upcoming lyrics since we already showed them)\n this.drawActiveLines(canvasWidth, canvasHeight, true); // true = skip upcoming lyrics\n }\n }\n\n drawInstrumentalIntro(canvasWidth, canvasHeight) {\n if (!this.lyrics || this.lyrics.length === 0) return;\n\n // Use interpolated time for smooth 60fps progress bar\n const now = this.getInterpolatedTime();\n const firstLine = this.lyrics[0];\n\n if (!firstLine) return;\n\n const introDuration = firstLine.startTime;\n const introProgress = now / introDuration;\n\n // Draw progress bar at top\n const barWidth = canvasWidth * 0.8;\n const barX = (canvasWidth - barWidth) / 2;\n const barY = 80;\n\n const _barInfo = this.drawProgressBar(\n barX,\n barY,\n barWidth,\n undefined,\n introProgress,\n canvasWidth\n );\n\n // Calculate where the upcoming lyric is being drawn\n const upcomingY = barY + this.settings.progressBarMargin;\n\n // Lock the first lyric as upcoming during intro\n if (this.lockedUpcomingIndex !== 0) {\n this.lockedUpcomingIndex = 0;\n }\n\n // Check if we should start the transition animation (0.3s before first lyric starts)\n // This ensures smooth transition from intro preview to active lyric\n this.startTransitionAnimations([], upcomingY);\n\n // Draw transitioning lyrics if animation has started\n for (const [lineIndex, transition] of this.lyricTransitions.entries()) {\n const lyricLine = this.lyrics[lineIndex];\n if (lyricLine) {\n this.drawTransitioningLine(lyricLine, canvasWidth, transition);\n }\n }\n\n // Draw upcoming first lyrics with proper spacing (if not transitioning)\n if (!this.lyricTransitions.has(0)) {\n this.drawUpcomingLyricsPreview(\n firstLine,\n canvasWidth,\n canvasHeight,\n introProgress,\n upcomingY\n );\n }\n }\n\n drawInstrumentalOutro(canvasWidth, canvasHeight) {\n // Find the last main singer line to calculate outro progress\n const lastMainLine = this.getLastMainSingerLine();\n if (!lastMainLine) return;\n\n // Use interpolated time for smooth 60fps progress bar\n const currentTime = this.getInterpolatedTime();\n const outroStartTime = lastMainLine.endTime;\n const outroLength = this.songDuration - outroStartTime;\n const outroProgress = Math.max(0, Math.min(1, (currentTime - outroStartTime) / outroLength));\n\n // Draw progress bar at top\n const _barInfo = this.drawProgressBar(\n undefined,\n undefined,\n undefined,\n undefined,\n outroProgress,\n canvasWidth\n );\n\n // Show outro message below progress bar\n this.ctx.fillStyle = this.settings.textColor;\n this.ctx.font = `${this.settings.fontSize}px ${this.settings.fontFamily}`;\n this.ctx.textAlign = 'center';\n\n const centerX = canvasWidth / 2;\n const centerY = canvasHeight / 2;\n\n this.drawTextWithBackground('♫ Instrumental Outro ♫', centerX, centerY);\n }\n\n drawBackupOnlyProgressBar(canvasWidth, canvasHeight) {\n if (!this.lyrics) return;\n\n // Use interpolated time for smooth 60fps progress bar\n const now = this.getInterpolatedTime();\n\n // Check if there's an active main singer at interpolated time\n // (prevents flash when interpolated time is ahead of reported time)\n for (let i = 0; i < this.lyrics.length; i++) {\n const line = this.lyrics[i];\n if (!line.isBackup && !line.isDisabled && now >= line.startTime && now <= line.endTime) {\n // There's an active main singer, don't show backup-only progress bar\n return;\n }\n }\n\n // Find the next main singer line\n let nextMainLine = null;\n for (let i = 0; i < this.lyrics.length; i++) {\n if (\n !this.lyrics[i].isBackup &&\n !this.lyrics[i].isDisabled &&\n now < this.lyrics[i].startTime\n ) {\n nextMainLine = this.lyrics[i];\n break;\n }\n }\n\n if (!nextMainLine) return;\n\n // Find when the backup-only period started (either song start or end of last main line)\n let gapStart = 0;\n for (let i = this.lyrics.length - 1; i >= 0; i--) {\n if (!this.lyrics[i].isBackup && !this.lyrics[i].isDisabled && this.lyrics[i].endTime <= now) {\n gapStart = this.lyrics[i].endTime;\n break;\n }\n }\n\n const gapDuration = nextMainLine.startTime - gapStart;\n\n // Only show progress bar for gaps longer than 5 seconds\n if (gapDuration <= 5) return;\n\n // Calculate progress\n const gapProgress = (now - gapStart) / gapDuration;\n\n // Draw progress bar at top\n const barWidth = canvasWidth * 0.8;\n const barX = (canvasWidth - barWidth) / 2;\n const barY = 80;\n\n const _barInfo = this.drawProgressBar(\n barX,\n barY,\n barWidth,\n undefined,\n gapProgress,\n canvasWidth\n );\n\n // Draw upcoming main lyrics preview\n this.drawUpcomingLyricsPreview(\n nextMainLine,\n canvasWidth,\n canvasHeight,\n gapProgress,\n barY + this.settings.progressBarMargin\n );\n\n // Still render any active backup singers below the progress bar\n this.drawActiveLines(canvasWidth, canvasHeight);\n }\n\n drawUpcomingLyricsPreview(nextLine, canvasWidth, canvasHeight, progress, startY) {\n if (!nextLine) return;\n\n // Get text from line (handle different KAI formats)\n let text = '';\n if (nextLine.text) {\n text = nextLine.text;\n } else if (nextLine.words && nextLine.words.length > 0) {\n text = nextLine.words.map((w) => w.text || w.word || w).join(' ');\n }\n\n if (!text || text.trim() === '') return;\n\n // Set font for upcoming lyrics (same size as current line)\n this.ctx.font = `${this.settings.fontSize}px ${this.settings.fontFamily}`;\n this.ctx.textAlign = 'center';\n\n // Determine color based on readiness\n const isReady = progress >= 1.0;\n this.ctx.fillStyle = isReady ? this.settings.activeColor : this.settings.upcomingColor;\n\n // Handle long text with proper wrapping\n const maxWidth = canvasWidth * 0.9;\n const words = text.split(' ');\n const lines = [];\n let currentLine = '';\n\n for (const word of words) {\n const testLine = currentLine ? currentLine + ' ' + word : word;\n const testWidth = this.ctx.measureText(testLine).width;\n\n if (testWidth <= maxWidth) {\n currentLine = testLine;\n } else {\n if (currentLine) {\n lines.push(currentLine);\n currentLine = word;\n } else {\n // Single word is too long, just add it anyway\n lines.push(word);\n }\n }\n }\n\n if (currentLine) {\n lines.push(currentLine);\n }\n\n // Draw each line below the progress bar (consistent with other functions)\n const lineSpacing = this.settings.lineHeight * 0.8;\n let currentY = startY + 60; // Start below the progress bar with some padding\n\n lines.forEach((line) => {\n this.drawTextWithBackground(line, canvasWidth / 2, currentY);\n currentY += lineSpacing;\n });\n }\n\n wrapWordsToLinesPreview(words, maxWidth) {\n const lines = [];\n let currentLine = [];\n let currentWidth = 0;\n\n words.forEach((word, index) => {\n const wordWidth = this.ctx.measureText(word).width;\n const spaceWidth = index > 0 ? this.settings.wordSpacing : 0;\n const totalWidth = currentWidth + spaceWidth + wordWidth;\n\n if (totalWidth <= maxWidth || currentLine.length === 0) {\n currentLine.push(word);\n currentWidth = totalWidth;\n } else {\n if (currentLine.length > 0) {\n lines.push(currentLine);\n }\n currentLine = [word];\n currentWidth = wordWidth;\n }\n });\n\n if (currentLine.length > 0) {\n lines.push(currentLine);\n }\n\n return lines;\n }\n\n drawWordLinePreview(words, centerX, y, maxWidth, textColor, isReady) {\n // Calculate total width of this line\n const totalWidth = words.reduce((width, word, index) => {\n const wordWidth = this.ctx.measureText(word).width;\n const spacing = index < words.length - 1 ? this.settings.wordSpacing : 0;\n return width + wordWidth + spacing;\n }, 0);\n\n // Start position for centering\n let x = centerX - totalWidth / 2;\n\n words.forEach((word, _index) => {\n this.ctx.fillStyle = textColor;\n this.ctx.textAlign = 'left';\n\n // Add subtle glow effect when ready\n if (isReady) {\n this.ctx.save();\n this.ctx.shadowColor = this.settings.activeColor;\n this.ctx.shadowBlur = 4;\n this.ctx.fillText(word, x, y);\n this.ctx.restore();\n } else {\n this.ctx.fillText(word, x, y);\n }\n\n // Move to next word position\n const wordWidth = this.ctx.measureText(word).width;\n x += wordWidth + this.settings.wordSpacing;\n });\n }\n\n drawTextWithBackground(text, x, y, glowColor = null) {\n // Measure text dimensions properly\n const metrics = this.ctx.measureText(text);\n const textWidth = metrics.width;\n const textAscent = metrics.actualBoundingBoxAscent || this.settings.fontSize * 0.8;\n const textDescent = metrics.actualBoundingBoxDescent || this.settings.fontSize * 0.2;\n const textHeight = textAscent + textDescent;\n\n // Calculate background rectangle with padding (10% larger)\n const padding = 12;\n const extraSize = 0.1; // 10% bigger\n const bgWidth = (textWidth + padding * 2) * (1 + extraSize);\n const bgHeight = (textHeight + padding) * (1 + extraSize);\n\n // Center the larger background around the text\n const bgX = x - bgWidth / 2;\n const bgY = y - textAscent - (bgHeight - textHeight) / 2;\n const borderRadius = 12;\n\n // Draw rounded background\n this.ctx.save();\n this.ctx.fillStyle = 'rgba(0, 0, 0, 0.65)';\n this.ctx.beginPath();\n this.ctx.roundRect(bgX, bgY, bgWidth, bgHeight, borderRadius);\n this.ctx.fill();\n this.ctx.restore();\n\n // Draw main text with optional glow for singer identification\n this.ctx.save();\n if (glowColor) {\n this.ctx.shadowColor = glowColor;\n this.ctx.shadowBlur = 12;\n this.ctx.shadowOffsetX = 0;\n this.ctx.shadowOffsetY = 0;\n }\n this.ctx.fillText(text, x, y);\n this.ctx.restore();\n }\n\n drawProgressBar(x, y, width, height, progress, canvasWidth) {\n // Default positioning if not provided\n const barX = x !== undefined ? x : 50;\n const barY = y !== undefined ? y : 50;\n const barWidth = width !== undefined ? width : canvasWidth - 100;\n const barHeight = height !== undefined ? height : this.settings.progressBarHeight;\n\n // Progress bar background\n this.ctx.fillStyle = this.settings.progressBarBg;\n this.ctx.fillRect(barX, barY, barWidth, barHeight);\n\n // Progress fill\n this.ctx.fillStyle = this.settings.progressBarColor;\n this.ctx.fillRect(barX, barY, barWidth * Math.max(0, Math.min(1, progress)), barHeight);\n\n return { barX, barY, barWidth, barHeight };\n }\n\n drawBouncingBall(x, word, lineY) {\n // Calculate ball position based on progress through word\n const progress = (this.currentTime - word.startTime) / (word.endTime - word.startTime);\n const wordWidth = this.ctx.measureText(word.text).width;\n\n const ballX = x + progress * wordWidth;\n const ballY = lineY - 30 + Math.sin(progress * Math.PI * 4) * 5; // Bouncing effect\n\n // Draw ball\n this.ctx.save();\n this.ctx.fillStyle = this.settings.ballColor;\n this.ctx.beginPath();\n this.ctx.arc(ballX, ballY, this.settings.ballSize, 0, Math.PI * 2);\n this.ctx.fill();\n this.ctx.restore();\n }\n\n // Draw song info when loaded but not playing\n drawSongInfo(width, height, songData) {\n const ctx = this.ctx;\n ctx.save();\n\n // Get song info from various possible locations\n const title =\n songData.title ||\n songData.metadata?.title ||\n songData.name?.replace('.kai', '') ||\n 'Unknown Title';\n const artist = songData.artist || songData.metadata?.artist || 'Unknown Artist';\n const requester = songData.requester;\n\n // Position higher on canvas (35% from top instead of centered)\n const centerX = width / 2;\n const centerY = height * 0.35;\n\n // Draw title\n ctx.fillStyle = '#ffffff';\n ctx.font = 'bold 72px Arial, sans-serif';\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n\n // Add text shadow for better visibility\n ctx.shadowColor = 'rgba(0, 0, 0, 0.8)';\n ctx.shadowBlur = 4;\n ctx.shadowOffsetX = 2;\n ctx.shadowOffsetY = 2;\n\n ctx.fillText(title, centerX, centerY - 50);\n\n // Draw artist and singer on same line\n ctx.font = '48px Arial, sans-serif';\n const artistY = centerY + 50;\n\n if (requester && requester.toUpperCase() !== 'KJ') {\n // Measure artist text to position singer to the right\n const artistText = artist;\n const singerText = ` - ${requester}`;\n\n ctx.fillStyle = '#cccccc';\n const artistWidth = ctx.measureText(artistText).width;\n const singerWidth = ctx.measureText(singerText).width;\n const totalWidth = artistWidth + singerWidth;\n\n // Draw centered as a group\n const startX = centerX - totalWidth / 2;\n\n ctx.fillText(artistText, startX, artistY);\n\n // Draw singer in yellow\n ctx.fillStyle = '#FCD34D'; // yellow-300 for non-KJ singers\n ctx.fillText(singerText, startX + artistWidth, artistY);\n } else {\n // Just artist, centered\n ctx.fillStyle = '#cccccc';\n ctx.fillText(artist, centerX, artistY);\n }\n\n ctx.restore();\n }\n\n destroy() {\n if (this.animationFrame) {\n cancelAnimationFrame(this.animationFrame);\n }\n\n // Clean up resize listener and observer\n if (this.resizeHandler) {\n window.removeEventListener('resize', this.resizeHandler);\n }\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = null;\n }\n\n // Destroy Butterchurn instance and ALL related components\n if (this.butterchurn && this.butterchurn.destroy) {\n this.butterchurn.destroy();\n }\n this.butterchurn = null;\n\n // Disconnect and clean up all Butterchurn audio nodes\n if (this.butterchurnSourceNode) {\n this.butterchurnSourceNode.disconnect();\n this.butterchurnSourceNode = null;\n }\n\n // Close Butterchurn audio context\n if (this.butterchurnAudioContext) {\n this.butterchurnAudioContext.close();\n this.butterchurnAudioContext = null;\n }\n\n // Clear all Butterchurn-related properties\n this.butterchurnAnalyser = null;\n this.butterchurnVisualAnalyser = null;\n this.butterchurnFrequencyData = null;\n this.butterchurnAudioBuffer = null;\n this.effectsCanvas = null;\n\n // Close waveform audio context\n if (this.audioContext) {\n this.audioContext.close();\n this.audioContext = null;\n }\n\n // Stop microphone capture properly\n this.stopMicrophoneCapture();\n\n // Clear WebGL context if available\n if (this.gl) {\n this.gl = null;\n }\n }\n\n reinitialize() {\n // Store current preferences\n const currentPreferences = { ...this.waveformPreferences };\n\n // Destroy everything\n this.destroy();\n\n // Reset state variables\n this.lyrics = null;\n this.songDuration = 0;\n this.currentTime = 0;\n this.isPlaying = false;\n this.cachedCurrentLine = -1;\n this.lastTimeForLineCalculation = -1;\n this.backupAnimations.clear();\n this.lastActiveSinger = null; // Reset for backup:PA feature\n\n // Restore preferences\n this.waveformPreferences = currentPreferences;\n\n // Reinitialize everything\n this.setupCanvas();\n this.setupAdvancedVisualizations();\n this.setupResponsiveCanvas();\n this.startAnimation();\n\n // Restart microphone if it was enabled (with delay to prevent issues)\n if (this.waveformPreferences.enableMic) {\n setTimeout(async () => {\n // Ensure the input device selection is properly restored before starting\n await this.ensureInputDeviceSelection();\n this.startMicrophoneCapture();\n }, 200); // Extra delay after reinitialize to let everything settle\n }\n }\n\n async ensureInputDeviceSelection() {\n try {\n // Load saved input device preference from settings API\n if (window.kaiAPI.settings) {\n const prefs = await window.kaiAPI.settings.get('devicePreferences');\n if (prefs && prefs.input && prefs.input.id) {\n this.inputDevice = prefs.input.id;\n }\n }\n } catch (error) {\n console.warn('Failed to load input device preference:', error);\n }\n }\n\n setShowUpcomingLyrics(enabled) {\n this.waveformPreferences.showUpcomingLyrics = enabled;\n }\n\n drawUpcomingLyrics(canvasWidth, canvasHeight, startY) {\n if (!this.lyrics) return;\n\n // Use interpolated time for precise upcoming lyric detection\n const now = this.getInterpolatedTime();\n const maxTimeAhead = 5.0; // Only show lyrics up to 5 seconds ahead\n\n // Check if current locked upcoming has become active - only then clear it\n if (this.lockedUpcomingIndex !== null && this.lockedUpcomingIndex !== undefined) {\n const lockedLine = this.lyrics[this.lockedUpcomingIndex];\n\n // Don't clear if this lyric is currently transitioning (prevents second lyric from flashing)\n const isTransitioning = this.lyricTransitions.has(this.lockedUpcomingIndex);\n\n // Only clear if the line doesn't exist or has actually become active AND is not transitioning\n if (!lockedLine || (now >= lockedLine.startTime && !isTransitioning)) {\n this.lockedUpcomingIndex = null;\n }\n }\n\n // If no locked upcoming or it became active, find the next one\n // BUT don't search if there are any transitioning lyrics (wait for them to finish)\n if (\n (this.lockedUpcomingIndex === null || this.lockedUpcomingIndex === undefined) &&\n this.lyricTransitions.size === 0\n ) {\n // Find the next upcoming lyric that starts after now\n let nextUpcomingIndex = null;\n let closestStartTime = Infinity;\n\n for (let i = 0; i < this.lyrics.length; i++) {\n const line = this.lyrics[i];\n if (\n !line.isDisabled &&\n !line.isBackup &&\n line.startTime > now &&\n line.startTime <= now + maxTimeAhead &&\n line.startTime < closestStartTime\n ) {\n nextUpcomingIndex = i;\n closestStartTime = line.startTime;\n }\n }\n\n this.lockedUpcomingIndex = nextUpcomingIndex;\n }\n\n // If still no upcoming, return\n if (this.lockedUpcomingIndex === null || this.lockedUpcomingIndex === undefined) return;\n\n const lockedLine = this.lyrics[this.lockedUpcomingIndex];\n if (!lockedLine) {\n this.lockedUpcomingIndex = null;\n return;\n }\n\n // Double-check it's not active (this shouldn't happen with the logic above)\n if (now >= lockedLine.startTime) {\n this.lockedUpcomingIndex = null;\n return;\n }\n\n // Don't draw if this lyric is currently transitioning/animating\n if (this.lyricTransitions.has(this.lockedUpcomingIndex)) {\n return;\n }\n\n // Draw the locked upcoming lyric\n this.ctx.save();\n this.ctx.font = `${this.settings.fontSize}px ${this.settings.fontFamily}`;\n this.ctx.textAlign = 'center';\n this.ctx.fillStyle = '#999999';\n this.ctx.globalAlpha = 0.8;\n\n const currentY = startY;\n\n // Get text from line\n let text = '';\n if (lockedLine.text) {\n text = lockedLine.text;\n } else if (lockedLine.words && lockedLine.words.length > 0) {\n text = lockedLine.words.map((w) => w.text || w.word || w).join(' ');\n }\n\n // Get glow color for non-lead singers (so you know whose line is coming up)\n const glowColor = lockedLine.singer ? this.getSingerColor(lockedLine) : null;\n\n if (text) {\n this.drawWrappedText(text, canvasWidth / 2, currentY, canvasWidth * 0.9, glowColor);\n }\n\n this.ctx.restore();\n }\n\n drawWrappedText(text, x, y, maxWidth, glowColor = null) {\n const words = text.split(' ');\n let currentLine = '';\n let linesRendered = 0;\n const lineHeight = this.settings.fontSize * 1.2; // Match font size with some line spacing\n\n for (let i = 0; i < words.length; i++) {\n const testLine = currentLine + (currentLine ? ' ' : '') + words[i];\n const testWidth = this.ctx.measureText(testLine).width;\n\n if (testWidth > maxWidth && currentLine) {\n // Draw current line and start new line\n this.drawTextWithBackground(currentLine, x, y, glowColor);\n y += lineHeight;\n linesRendered++;\n currentLine = words[i];\n } else {\n currentLine = testLine;\n }\n }\n\n // Draw the final line\n if (currentLine) {\n this.drawTextWithBackground(currentLine, x, y, glowColor);\n linesRendered++;\n }\n\n return linesRendered;\n }\n\n updateLyricTransitions(currentActiveLines, now, currentActiveEndY) {\n // Get current upcoming lyrics\n const upcomingLines = [];\n if (this.lyrics) {\n for (let i = 0; i < this.lyrics.length; i++) {\n const line = this.lyrics[i];\n if (\n !line.isDisabled &&\n !line.isBackup &&\n line.startTime > now &&\n line.startTime <= now + 5.0\n ) {\n upcomingLines.push({ ...line, index: i });\n break; // Only track the very next one for transitions\n }\n }\n }\n\n // Check for lyrics that should start transitioning (0.5 seconds before they become active)\n for (const upcomingLine of upcomingLines) {\n const timeToStart = upcomingLine.startTime - now;\n if (timeToStart <= 0.5 && timeToStart > 0 && !this.lyricTransitions.has(upcomingLine.index)) {\n // Use the EXACT position where the upcoming line was just displayed\n let upcomingPosition;\n if (\n this.lastUpcomingDisplayY !== null &&\n this.lastUpcomingLineIndex === upcomingLine.index\n ) {\n upcomingPosition = this.lastUpcomingDisplayY;\n } else {\n // Fallback calculation\n upcomingPosition = currentActiveEndY + 50;\n }\n\n const activePosition = this.canvas.height / 2 - 180; // Where active is shown (higher up)\n\n this.lyricTransitions.set(upcomingLine.index, {\n startTime: now,\n duration: this.settings.lyricTransitionDuration,\n progress: 0,\n startY: upcomingPosition, // EXACT position where it was displayed\n endY: activePosition, // Higher on screen (lower Y value)\n });\n }\n }\n\n // Update existing transitions\n for (const [lineIndex, transition] of this.lyricTransitions.entries()) {\n const elapsed = now - transition.startTime;\n\n // Update progress (let transitions complete naturally)\n const newProgress = Math.min(1.0, elapsed / transition.duration);\n transition.progress = newProgress;\n\n // Remove completed transitions\n if (transition.progress >= 1.0) {\n this.lyricTransitions.delete(lineIndex);\n }\n }\n }\n\n startTransitionAnimations(activeLines, upcomingY) {\n // Use interpolated time for smooth 60fps lyric slide animations\n const now = this.getInterpolatedTime();\n\n // Check if locked upcoming lyric should start animating\n if (this.lockedUpcomingIndex !== null && this.lockedUpcomingIndex !== undefined) {\n const upcomingLine = this.lyrics[this.lockedUpcomingIndex];\n if (upcomingLine) {\n const timeUntilActive = upcomingLine.startTime - now;\n\n // Start animation before the lyric becomes active\n if (timeUntilActive <= this.settings.lyricTransitionStartBefore && timeUntilActive > 0) {\n // Only start if not already animating\n if (!this.lyricTransitions.has(this.lockedUpcomingIndex)) {\n // Calculate active position (same as drawActiveLines)\n const canvasHeight = this.canvas.height;\n const lineSpacing = this.settings.lineHeight * 1.2;\n\n // Check if there are any current main lines (to calculate proper position)\n const currentMainLines = activeLines.filter((line) => !line.isBackup);\n const totalLines = Math.max(1, currentMainLines.length);\n const totalHeight = totalLines * lineSpacing;\n\n // This matches the exact calculation in drawActiveLines\n const activeY = canvasHeight / 2 - totalHeight / 2 + lineSpacing - 180;\n\n // Start the transition\n this.lyricTransitions.set(this.lockedUpcomingIndex, {\n startTime: now,\n duration: this.settings.lyricTransitionDuration,\n progress: 0,\n startY: upcomingY, // Where upcoming lyric is drawn (no offset)\n endY: activeY,\n });\n\n // Hide any currently active main lines that will overlap with this transition\n // Check all currently active lines (not just the filtered activeLines array)\n for (let i = 0; i < this.lyrics.length; i++) {\n const line = this.lyrics[i];\n if (!line.isDisabled && !line.isBackup && i !== this.lockedUpcomingIndex) {\n // Check if this line is currently active\n if (now >= line.startTime && now <= line.endTime) {\n const timeUntilEnd = line.endTime - now;\n // If active line ends during the transition period, hide it immediately\n if (timeUntilEnd > 0 && timeUntilEnd <= this.settings.lyricTransitionDuration) {\n this.hiddenDuringTransition.add(i);\n }\n }\n }\n }\n }\n }\n }\n }\n\n // Update existing transitions\n for (const [lineIndex, transition] of this.lyricTransitions.entries()) {\n const elapsed = now - transition.startTime;\n\n // Update progress\n transition.progress = Math.min(1.0, elapsed / transition.duration);\n\n // Remove completed transitions (let them finish naturally)\n if (transition.progress >= 1.0) {\n this.lyricTransitions.delete(lineIndex);\n }\n }\n\n // Clean up hidden lines that are no longer active\n for (const hiddenIndex of this.hiddenDuringTransition) {\n const line = this.lyrics[hiddenIndex];\n if (line && (now > line.endTime || now < line.startTime)) {\n this.hiddenDuringTransition.delete(hiddenIndex);\n }\n }\n }\n\n drawTransitioningLine(line, canvasWidth, transition) {\n // Simple linear interpolation for position - THIS IS THE ANIMATION\n const currentY =\n transition.startY + (transition.endY - transition.startY) * transition.progress;\n\n // Get the singer's target color (instead of hardcoded blue)\n const targetColorHex = this.getSingerColor(line);\n const endColor = this.hexToRgb(targetColorHex);\n\n // Interpolate color from upcoming grey to singer's active color\n const startColor = { r: 136, g: 136, b: 136 }; // #888888 (upcoming grey from settings)\n\n const r = Math.round(startColor.r + (endColor.r - startColor.r) * transition.progress);\n const g = Math.round(startColor.g + (endColor.g - startColor.g) * transition.progress);\n const b = Math.round(startColor.b + (endColor.b - startColor.b) * transition.progress);\n\n // Interpolate alpha\n const alpha = 0.8 + (1.0 - 0.8) * transition.progress;\n\n // Set up context for animated line\n this.ctx.save();\n this.ctx.font = `${this.settings.fontSize}px ${this.settings.fontFamily}`;\n this.ctx.textAlign = 'center';\n this.ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;\n\n // Get text from line\n let text = '';\n if (line.text) {\n text = line.text;\n } else if (line.words && line.words.length > 0) {\n text = line.words.map((w) => w.text || w.word || w).join(' ');\n }\n\n // Get glow color for non-lead singers during transition\n const glowColor = line.singer ? targetColorHex : null;\n\n if (text) {\n // Handle word wrapping during transition to prevent layout jumps\n const maxWidth = canvasWidth * 0.9;\n const words = text.split(' ');\n const lines = [];\n let currentLine = '';\n\n for (const word of words) {\n const testLine = currentLine ? currentLine + ' ' + word : word;\n const testWidth = this.ctx.measureText(testLine).width;\n\n if (testWidth <= maxWidth) {\n currentLine = testLine;\n } else {\n if (currentLine) {\n lines.push(currentLine);\n currentLine = word;\n } else {\n lines.push(word);\n }\n }\n }\n\n if (currentLine) {\n lines.push(currentLine);\n }\n\n // Draw each wrapped line (match drawSingleLine spacing)\n const lineHeight = this.settings.lineHeight * 0.8;\n lines.forEach((textLine, index) => {\n const adjustedY = currentY + index * lineHeight;\n this.drawTextWithBackground(textLine, canvasWidth / 2, adjustedY, glowColor);\n });\n }\n\n this.ctx.restore();\n }\n\n // Helper to convert hex color to RGB object\n hexToRgb(hex) {\n // Default to cyan if invalid\n const defaultColor = { r: 0, g: 191, b: 255 };\n if (!hex || typeof hex !== 'string') return defaultColor;\n\n // Remove # if present\n hex = hex.replace(/^#/, '');\n\n // Parse 3 or 6 digit hex\n if (hex.length === 3) {\n hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];\n }\n\n if (hex.length !== 6) return defaultColor;\n\n const num = parseInt(hex, 16);\n return {\n r: (num >> 16) & 255,\n g: (num >> 8) & 255,\n b: num & 255,\n };\n }\n}\n\n// Export removed - KaraokeRenderer is used by PlayerController\n// No longer attached to window global\n","// CDGraphics will be loaded from node_modules via webpack or as a global\n// For now, we'll load it dynamically when needed\n\n/* global CDGraphics */\n\nimport { PlayerInterface } from './PlayerInterface.js';\nimport { MicrophoneEngine } from './microphoneEngine.js';\n\nexport class CDGPlayer extends PlayerInterface {\n constructor(canvasId) {\n super(); // Call PlayerInterface constructor\n\n this.canvas = document.getElementById(canvasId);\n\n if (!this.canvas) {\n console.error('CDG canvas not found:', canvasId);\n return;\n }\n\n this.ctx = this.canvas.getContext('2d');\n this.cdgPlayer = null;\n this.cdgData = null;\n // Note: this.isPlaying is inherited from PlayerInterface\n this.currentTime = 0;\n this.animationFrame = null;\n\n // CDG output canvas (300x216)\n this.cdgCanvas = document.createElement('canvas');\n this.cdgCanvas.width = 300;\n this.cdgCanvas.height = 216;\n this.cdgCtx = this.cdgCanvas.getContext('2d');\n\n // Web Audio API for MP3 playback (will be set by main.js)\n this.audioContext = null;\n this.audioSource = null;\n this.audioBuffer = null;\n this.startTime = 0;\n this.pauseTime = 0;\n this.gainNode = null;\n this.analyserNode = null;\n\n // Background effects (Butterchurn)\n this.effectsCanvas = null;\n this.butterchurn = null;\n this.effectsEnabled = true;\n this.overlayOpacity = 0.7; // Default, will be updated from settings\n\n // Microphone engine (handles mic input and auto-tune)\n this.micEngine = null; // Will be initialized when audio context is set\n\n // QR code for server URL\n this.qrCodeCanvas = null;\n this.showQrCode = false;\n this.serverUrl = null;\n\n // Queue display\n this.queueItems = [];\n this.displayQueue = true;\n\n // Note: this.stateReportInterval is inherited from PlayerInterface\n }\n\n /**\n * Set server URL and generate QR code\n * @param {string} url - Server URL\n * @param {boolean} show - Whether to show QR code\n */\n async setServerQRCode(url, show) {\n this.serverUrl = url;\n this.showQrCode = show;\n\n if (url && show) {\n try {\n // Dynamically import QR code generator\n const { generateQRCodeCanvas } = await import('../utils/qrCodeGenerator.js');\n this.qrCodeCanvas = await generateQRCodeCanvas(url, 150);\n } catch (error) {\n console.error('Error generating QR code:', error);\n this.qrCodeCanvas = null;\n }\n } else {\n this.qrCodeCanvas = null;\n }\n }\n\n /**\n * Set queue items and display setting\n * @param {Array} queue - Array of queue items with title, artist, requester\n * @param {boolean} display - Whether to display queue\n */\n setQueueDisplay(queue, display) {\n this.queueItems = queue || [];\n this.displayQueue = display !== false;\n }\n\n setOverlayOpacity(opacity) {\n this.overlayOpacity = opacity;\n }\n\n /**\n * Implements PlayerInterface.loadSong()\n * @param {Object} songData - CDG song data\n * @returns {Promise<boolean>} Success status\n */\n async loadSong(_songData) {\n try {\n this.cdgData = _songData;\n\n // Reset position using base class method\n this.resetPosition();\n\n // Reset CDG-specific timing state\n this.currentTime = 0;\n this.startTime = 0;\n this.pauseTime = 0;\n\n // Load CDGraphics library dynamically\n if (typeof CDGraphics === 'undefined') {\n console.error('💿 CDGraphics library not loaded');\n throw new Error('CDGraphics library not available');\n }\n\n // Load CDG file data - convert to ArrayBuffer first\n const cdgBuffer = _songData.cdg.data;\n\n // Convert to ArrayBuffer\n let arrayBuffer;\n if (cdgBuffer instanceof Uint8Array || cdgBuffer instanceof Buffer) {\n // Create a new ArrayBuffer and copy data\n arrayBuffer = new ArrayBuffer(cdgBuffer.length || cdgBuffer.byteLength);\n const view = new Uint8Array(arrayBuffer);\n view.set(cdgBuffer);\n } else if (cdgBuffer.buffer instanceof ArrayBuffer) {\n // It's a typed array with an ArrayBuffer\n arrayBuffer = cdgBuffer.buffer.slice(\n cdgBuffer.byteOffset,\n cdgBuffer.byteOffset + cdgBuffer.byteLength\n );\n } else if (cdgBuffer instanceof ArrayBuffer) {\n // It's already an ArrayBuffer\n arrayBuffer = cdgBuffer;\n } else {\n console.error('💿 Unknown buffer type:', cdgBuffer);\n throw new Error('Unknown buffer type');\n }\n\n // Initialize CDGraphics player with the ArrayBuffer\n this.cdgPlayer = new CDGraphics(arrayBuffer);\n\n // Decode MP3 audio buffer using Web Audio API\n // Audio context will be set by main.js before loading\n if (!this.audioContext) {\n throw new Error('Audio context not set. Call setAudioContext() first.');\n }\n\n const mp3ArrayBuffer = _songData.audio.mp3.buffer.slice(\n _songData.audio.mp3.byteOffset,\n _songData.audio.mp3.byteOffset + _songData.audio.mp3.byteLength\n );\n this.audioBuffer = await this.audioContext.decodeAudioData(mp3ArrayBuffer);\n\n return true;\n } catch (error) {\n console.error('💿 Failed to load CDG:', error);\n return false;\n }\n }\n\n play() {\n if (!this.cdgPlayer || !this.audioBuffer) {\n console.warn('💿 No CDG loaded');\n return;\n }\n\n this.isPlaying = true;\n\n // Stop existing source if any (and clear its onended handler)\n if (this.audioSource) {\n this.audioSource.onended = null; // Clear handler before stopping\n try {\n this.audioSource.stop();\n } catch {\n // Already stopped\n }\n this.audioSource = null;\n }\n\n // Create new audio source\n this.audioSource = this.audioContext.createBufferSource();\n this.audioSource.buffer = this.audioBuffer;\n\n // Connect to gain node (which is connected to PA output)\n this.audioSource.connect(this.gainNode);\n\n // Also connect to analyser for Butterchurn\n if (this.analyserNode) {\n this.audioSource.connect(this.analyserNode);\n }\n\n // Connect to microphone engine for real-time music pitch detection (auto-tune)\n if (this.micEngine) {\n this.micEngine.connectMusicSource(this.audioSource);\n }\n\n // Handle song end - check both isPlaying AND that we've reached the end naturally\n const duration = this.audioBuffer.duration;\n this.audioSource.onended = () => {\n const currentPos = this.getCurrentTime();\n // Only treat as ended if we're near the end of the song (within 1 second)\n if (this.isPlaying && currentPos >= duration - 1) {\n this.handleSongEnd();\n }\n };\n\n // Start playback from current position\n const offset = this.pauseTime || 0;\n this.audioSource.start(0, offset);\n this.startTime = this.audioContext.currentTime - offset;\n\n this.startRendering();\n\n // Start state reporting\n this.startStateReporting();\n\n // Update microphone engine playing state\n if (this.micEngine) {\n this.micEngine.setPlaying(true);\n }\n\n // Report immediate state change\n this.reportStateChange();\n }\n\n pause() {\n this.isPlaying = false;\n\n // Store current position before stopping\n this.pauseTime = this.getCurrentTime();\n\n // Stop state reporting\n this.stopStateReporting();\n\n // Update microphone engine playing state\n if (this.micEngine) {\n this.micEngine.setPlaying(false);\n }\n\n // Report paused state\n this.reportStateChange();\n\n // Stop audio source (and clear onended handler to prevent false song-end events)\n if (this.audioSource) {\n this.audioSource.onended = null; // Clear handler first\n\n // Disconnect from music analysis\n if (this.micEngine) {\n this.micEngine.disconnectMusicSource(this.audioSource);\n }\n\n try {\n this.audioSource.stop();\n } catch {\n // Already stopped\n }\n this.audioSource = null;\n }\n\n this.stopRendering();\n }\n\n seek(positionSec) {\n const wasPlaying = this.isPlaying;\n\n // Pause if playing\n if (wasPlaying) {\n this.pause();\n }\n\n // Set new position\n this.pauseTime = positionSec;\n this.currentTime = positionSec;\n\n // Resume if it was playing\n if (wasPlaying) {\n this.play();\n } else {\n // Force a frame render at new position\n this.renderFrame();\n }\n }\n\n startRendering() {\n if (this.animationFrame) return;\n\n const render = () => {\n if (!this.isPlaying) return;\n\n this.renderFrame();\n this.animationFrame = requestAnimationFrame(render);\n };\n\n render();\n }\n\n stopRendering() {\n if (this.animationFrame) {\n cancelAnimationFrame(this.animationFrame);\n this.animationFrame = null;\n }\n }\n\n renderFrame() {\n if (!this.cdgPlayer) return;\n\n // Get current time from Web Audio API\n this.currentTime = this.getCurrentTime();\n\n // Get CDG frame for current time\n const result = this.cdgPlayer.render(this.currentTime);\n\n if (!result || !result.imageData) {\n console.warn('💿 No frame data at time:', this.currentTime);\n return;\n }\n\n // Make CDG background transparent for Butterchurn to show through\n const imageData = result.imageData;\n const data = imageData.data;\n\n // Get the background color from the CDG result (typically index 0 in palette)\n // The backgroundColor is usually at position 0,0\n const bgR = data[0];\n const bgG = data[1];\n const bgB = data[2];\n\n // Store background color for overlay\n this.cdgBackgroundColor = { r: bgR, g: bgG, b: bgB };\n\n // Make all pixels matching the background color transparent\n for (let i = 0; i < data.length; i += 4) {\n const r = data[i];\n const g = data[i + 1];\n const b = data[i + 2];\n\n // If pixel matches background color, make it transparent\n if (r === bgR && g === bgG && b === bgB) {\n data[i + 3] = 0; // Set alpha to 0 (transparent)\n }\n }\n\n // Convert modified ImageData to canvas\n this.cdgCtx.putImageData(imageData, 0, 0);\n\n // Clear main canvas\n this.ctx.fillStyle = '#000';\n this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);\n\n // Draw Butterchurn background effects if enabled\n if (this.effectsEnabled && this.effectsCanvas && this.butterchurn) {\n try {\n this.butterchurn.render();\n this.ctx.drawImage(this.effectsCanvas, 0, 0, this.canvas.width, this.canvas.height);\n } catch {\n // Effects rendering can fail, don't crash the whole renderer\n }\n }\n\n // Draw CDG background color as semi-transparent overlay (like KAI renderer does)\n // This respects the overlayOpacity setting for consistent look\n const overlayOpacity = this.overlayOpacity || 0.7;\n this.ctx.save();\n this.ctx.globalAlpha = overlayOpacity;\n this.ctx.fillStyle = `rgb(${bgR}, ${bgG}, ${bgB})`;\n this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);\n this.ctx.restore();\n\n // Scale and center CDG graphics on top\n // CDG is 300x216, scale 5x to 1500x1080 for 1080p\n const scale = 5;\n const cdgWidth = 300 * scale; // 1500\n const cdgHeight = 216 * scale; // 1080\n\n // Center horizontally in 1920px canvas (210px margins on each side)\n const offsetX = (this.canvas.width - cdgWidth) / 2;\n const offsetY = 0; // Fill height\n\n // Draw scaled CDG graphics on top (text and graphics only, no background)\n this.ctx.imageSmoothingEnabled = false; // Pixel-perfect scaling\n this.ctx.drawImage(this.cdgCanvas, offsetX, offsetY, cdgWidth, cdgHeight);\n\n // Draw QR code overlay if enabled\n this.drawQRCodeOverlay();\n\n // Draw queue display if enabled\n this.drawQueueDisplay();\n }\n\n /**\n * Draw QR code in bottom left corner (only when not playing)\n */\n drawQRCodeOverlay() {\n // Only show when not playing\n if (!this.showQrCode || !this.qrCodeCanvas || this.isPlaying) {\n return;\n }\n\n const padding = 20;\n const qrSize = 150;\n const x = padding; // Bottom left instead of right\n const y = this.canvas.height - qrSize - padding;\n\n // Draw white background with shadow\n this.ctx.save();\n this.ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';\n this.ctx.shadowBlur = 10;\n this.ctx.shadowOffsetX = 2;\n this.ctx.shadowOffsetY = 2;\n this.ctx.fillStyle = '#FFFFFF';\n this.ctx.fillRect(x - 10, y - 10, qrSize + 20, qrSize + 20);\n this.ctx.restore();\n\n // Draw QR code\n this.ctx.drawImage(this.qrCodeCanvas, x, y, qrSize, qrSize);\n }\n\n /**\n * Draw queue display in bottom right corner (only when not playing)\n */\n drawQueueDisplay() {\n // Only show when setting is enabled and queue has items\n if (!this.displayQueue || !this.queueItems || this.queueItems.length === 0 || this.isPlaying) {\n return;\n }\n\n const width = this.canvas.width;\n const height = this.canvas.height;\n const padding = 120; // Move further from edge (left)\n const bottomPadding = 80; // Move up from bottom\n const rightX = width - padding;\n const lineHeight = 64;\n const labelFontSize = 48;\n const songFontSize = 40;\n\n this.ctx.save();\n\n // Calculate text dimensions for background\n this.ctx.font = `bold ${labelFontSize}px sans-serif`;\n const labelText = 'Next up:';\n const labelWidth = this.ctx.measureText(labelText).width;\n\n // Measure all song texts and prepare data\n let maxWidth = labelWidth;\n const songData = this.queueItems.slice(0, 3).map((item) => {\n const title = item.title || item.song?.title || 'Unknown';\n const singer = item.requester || item.singer || '';\n\n // Measure title\n this.ctx.font = `${songFontSize}px sans-serif`;\n const titleWidth = this.ctx.measureText(title).width;\n\n // Measure singer if present\n let singerWidth = 0;\n if (singer) {\n const singerText = ` - ${singer}`;\n singerWidth = this.ctx.measureText(singerText).width;\n }\n\n const totalWidth = titleWidth + singerWidth;\n maxWidth = Math.max(maxWidth, totalWidth);\n\n return { title, singer };\n });\n\n // Calculate background dimensions\n const bgWidth = maxWidth + 30;\n const bgHeight = lineHeight + songData.length * lineHeight + 20;\n const bgX = rightX - bgWidth;\n const bgY = height - bgHeight - bottomPadding;\n\n // Draw semi-transparent background with shadow and rounded corners\n this.ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';\n this.ctx.shadowBlur = 10;\n this.ctx.shadowOffsetX = 2;\n this.ctx.shadowOffsetY = 2;\n this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';\n\n // Draw rounded rectangle\n const radius = 10;\n this.ctx.beginPath();\n this.ctx.moveTo(bgX + radius, bgY);\n this.ctx.lineTo(bgX + bgWidth - radius, bgY);\n this.ctx.quadraticCurveTo(bgX + bgWidth, bgY, bgX + bgWidth, bgY + radius);\n this.ctx.lineTo(bgX + bgWidth, bgY + bgHeight - radius);\n this.ctx.quadraticCurveTo(\n bgX + bgWidth,\n bgY + bgHeight,\n bgX + bgWidth - radius,\n bgY + bgHeight\n );\n this.ctx.lineTo(bgX + radius, bgY + bgHeight);\n this.ctx.quadraticCurveTo(bgX, bgY + bgHeight, bgX, bgY + bgHeight - radius);\n this.ctx.lineTo(bgX, bgY + radius);\n this.ctx.quadraticCurveTo(bgX, bgY, bgX + radius, bgY);\n this.ctx.closePath();\n this.ctx.fill();\n\n this.ctx.shadowColor = 'transparent';\n\n // Draw \"Next up:\" label in blue\n this.ctx.font = `bold ${labelFontSize}px sans-serif`;\n this.ctx.fillStyle = '#3B82F6'; // Tailwind blue-600\n this.ctx.textAlign = 'left';\n this.ctx.fillText(labelText, bgX + 15, bgY + labelFontSize + 10);\n\n // Draw queue items\n this.ctx.font = `${songFontSize}px sans-serif`;\n songData.forEach((item, index) => {\n const textY = bgY + labelFontSize + 10 + (index + 1) * lineHeight;\n const textX = bgX + 15;\n\n // Draw title in white\n this.ctx.fillStyle = '#FFFFFF';\n this.ctx.fillText(item.title, textX, textY);\n\n // Draw singer in yellow if present and not \"KJ\"\n if (item.singer) {\n const titleWidth = this.ctx.measureText(item.title).width;\n const isKJ = item.singer.toUpperCase() === 'KJ';\n this.ctx.fillStyle = isKJ ? '#FFFFFF' : '#FCD34D'; // yellow-300 for non-KJ singers\n this.ctx.fillText(` - ${item.singer}`, textX + titleWidth, textY);\n }\n });\n\n this.ctx.restore();\n }\n\n handleSongEnd() {\n this.stopRendering();\n\n // Use base class method for consistent song end handling\n this._triggerSongEnd();\n\n // Notify main process (for backward compatibility)\n if (window.electronAPI && window.electronAPI.queue) {\n window.electronAPI.queue.notifyComplete();\n }\n }\n\n setEffectsCanvas(canvas, butterchurn) {\n this.effectsCanvas = canvas;\n this.butterchurn = butterchurn;\n }\n\n setEffectsEnabled(enabled) {\n this.effectsEnabled = enabled;\n }\n\n getCurrentTime() {\n if (this.isPlaying && this.audioContext) {\n return this.audioContext.currentTime - this.startTime;\n }\n return this.pauseTime || 0;\n }\n\n /**\n * Implements PlayerInterface method - alias for getCurrentTime()\n * @returns {number} Current position in seconds\n */\n getCurrentPosition() {\n return this.getCurrentTime();\n }\n\n getDuration() {\n return this.audioBuffer ? this.audioBuffer.duration : 0;\n }\n\n /**\n * Note: reportStateChange(), startStateReporting(), and stopStateReporting()\n * are inherited from PlayerInterface base class\n */\n\n async setAudioContext(audioContext, gainNode, analyserNode) {\n this.audioContext = audioContext;\n this.gainNode = gainNode;\n this.analyserNode = analyserNode;\n\n // Initialize microphone engine with PA context\n this.micEngine = new MicrophoneEngine(audioContext, gainNode, {\n getCurrentPosition: () => this.getCurrentPosition(),\n });\n\n // Load auto-tune worklets\n await this.micEngine.loadAutoTuneWorklet();\n }\n\n async loadAutoTuneWorklet() {\n if (this.micEngine) {\n await this.micEngine.loadAutoTuneWorklet();\n }\n }\n\n async startMicrophoneInput(deviceId = 'default') {\n if (this.micEngine) {\n await this.micEngine.startMicrophoneInput(deviceId);\n }\n }\n\n enableAutoTune() {\n if (this.micEngine) {\n this.micEngine.enableAutoTune();\n }\n }\n\n disableAutoTune() {\n if (this.micEngine) {\n this.micEngine.disableAutoTune();\n }\n }\n\n setAutoTuneSettings(settings) {\n if (this.micEngine) {\n this.micEngine.setAutoTuneSettings(settings);\n }\n }\n\n stopMicrophoneInput() {\n if (this.micEngine) {\n this.micEngine.stopMicrophoneInput();\n }\n }\n\n setMicToSpeakers(enabled) {\n if (this.micEngine) {\n this.micEngine.setMicToSpeakers(enabled);\n }\n }\n\n async setEnableMic(enabled) {\n if (this.micEngine) {\n await this.micEngine.setEnableMic(enabled);\n }\n }\n\n setMicrophoneGain(gainValue) {\n if (this.micEngine) {\n this.micEngine.setMicrophoneGain(gainValue);\n }\n }\n\n destroy() {\n super.destroy(); // Call parent cleanup (stops state reporting)\n\n // Stop microphone engine\n if (this.micEngine) {\n this.micEngine.stopMicrophoneInput();\n this.micEngine = null;\n }\n\n this.stopRendering();\n if (this.audioSource) {\n // Disconnect from music analysis\n if (this.micEngine) {\n this.micEngine.disconnectMusicSource(this.audioSource);\n }\n\n try {\n this.audioSource.stop();\n } catch {\n // Already stopped\n }\n this.audioSource = null;\n }\n this.audioBuffer = null;\n this.cdgPlayer = null;\n this.cdgData = null;\n }\n\n /**\n * Get the format type this player handles\n * @returns {string} Format name\n */\n getFormat() {\n return 'cdg';\n }\n}\n","import { KaraokeRenderer } from './karaokeRenderer.js';\nimport { CDGPlayer } from './cdgPlayer.js';\n\nexport class PlayerController {\n constructor(kaiPlayer = null) {\n this.kaiPlayer = kaiPlayer;\n // lyricsContainer removed - KaraokeRenderer handles canvas-based lyrics now\n\n // Initialize karaoke renderer for KAI format lyrics\n this.karaokeRenderer = new KaraokeRenderer('karaokeCanvas');\n\n // Set up singer change callback for backup:PA feature\n // When a line with singer=\"backup:PA\" becomes active, route vocals to PA\n this.karaokeRenderer.onSingerChange = (singer) => {\n if (this.kaiPlayer) {\n const shouldEnableVocalsPA = singer === 'backup:PA';\n this.kaiPlayer.setVocalsPAEnabled(shouldEnableVocalsPA);\n }\n };\n\n // Initialize CDG player for CDG format (audio + graphics)\n this.cdgPlayer = new CDGPlayer('karaokeCanvas');\n\n // Track current format and active player\n this.currentFormat = null; // 'kai' or 'cdg'\n this.currentPlayer = null; // Reference to active PlayerInterface instance\n\n // Ensure canvas is properly sized after initialization\n setTimeout(() => {\n if (this.karaokeRenderer && this.karaokeRenderer.resizeHandler) {\n this.karaokeRenderer.resizeHandler();\n }\n }, 200);\n\n // DOM element references removed - React PlayerControls handles time/progress display now\n // Progress bar click-to-seek handled by React PlayerControls\n // Time display handled by React PlayerControls\n\n this.isPlaying = false;\n\n this.init();\n }\n\n init() {\n this.setupEventListeners();\n\n this.updateTimer = setInterval(() => {\n if (this.isPlaying) {\n this.updatePosition();\n // if (Math.random() < 0.05) { // Debug occasionally\n // }\n }\n }, 100);\n }\n\n setupEventListeners() {\n // Progress bar click-to-seek now handled by React PlayerControls component\n // Transport controls (play/pause/restart/next) handled by React TransportControlsWrapper\n }\n\n onSongLoaded(metadata) {\n // Store song metadata for display\n if (this.karaokeRenderer && metadata) {\n this.karaokeRenderer.setSongMetadata({\n title: metadata.title,\n artist: metadata.artist,\n requester: metadata.requester,\n });\n }\n\n // Get duration from player for karaokeRenderer\n let duration = this.currentPlayer?.getDuration() || metadata?.duration || 0;\n\n // If still zero, try to estimate from lyrics end time as fallback\n if (duration === 0 && metadata?.lyrics && Array.isArray(metadata.lyrics)) {\n let maxLyricTime = 0;\n for (const line of metadata.lyrics) {\n const endTime = line.end || line.end_time || (line.start || line.time || 0) + 3;\n maxLyricTime = Math.max(maxLyricTime, endTime);\n }\n if (maxLyricTime > 0) {\n duration = maxLyricTime + 10; // Add some padding\n }\n }\n\n // Load lyrics into karaoke renderer\n const lyrics = metadata?.lyrics || null;\n if (lyrics) {\n this.karaokeRenderer.loadLyrics(lyrics, duration);\n // Initial render at position 0 to show title\n this.karaokeRenderer.setCurrentTime(0);\n }\n\n // Load vocals audio data for waveform visualization\n if (metadata?.audio?.sources) {\n const vocalsSource = metadata.audio.sources.find(\n (source) => source.name === 'vocals' || source.filename?.includes('vocals')\n );\n\n if (vocalsSource && vocalsSource.audioData) {\n this.karaokeRenderer.setVocalsAudio(vocalsSource.audioData);\n } else {\n // No vocals track available - waveform disabled\n }\n\n // Note: Butterchurn visualization now uses PA analyser from kaiPlayer\n // Connected in songLoaders.js during song load\n // No need to decode mixdown buffer separately\n } else {\n // No audio sources available - waveforms disabled\n }\n\n // Display updates handled by React PlayerControls via IPC state\n\n // Ensure we're in stopped state\n this.pause();\n }\n\n // renderLyrics() method removed - KaraokeRenderer handles canvas-based lyrics now\n\n updatePosition() {\n // Update karaokeRenderer for lyrics sync ONLY\n // (PlayerInterface handles state broadcasting, song end detection, UI updates)\n if (this.currentPlayer && this.karaokeRenderer) {\n const position = this.currentPlayer.getCurrentPosition();\n this.karaokeRenderer.setCurrentTime(position);\n }\n }\n\n // updateTimeDisplay() method removed - React PlayerControls handles time display via IPC state\n // updateProgressBar() method removed - React PlayerControls handles progress bar via IPC state\n\n // updateActiveLyrics() method removed - KaraokeRenderer handles canvas-based lyrics now\n // seekToProgressPosition() method removed - React PlayerControls handles click-to-seek now\n\n async setPosition(positionSec) {\n if (!this.currentPlayer) return;\n\n // Bounds check using player's duration\n const duration = this.currentPlayer.getDuration();\n const boundedPosition = Math.max(0, Math.min(duration, positionSec));\n\n // Seek player\n try {\n await this.currentPlayer.seek(boundedPosition);\n } catch (error) {\n console.error('Seek error:', error);\n }\n\n // Update karaoke renderer immediately for lyrics sync\n if (this.karaokeRenderer) {\n this.karaokeRenderer.setCurrentTime(boundedPosition);\n // Reset the locked upcoming lyric so it recalculates based on new position\n this.karaokeRenderer.lockedUpcomingIndex = null;\n }\n\n // Player engine will broadcast new position via reportStateChange() for UI updates\n }\n\n async play() {\n this.isPlaying = true;\n\n // Update renderer's current time BEFORE starting playback\n // This ensures butterchurn starts from the correct position on resume\n if (this.currentPlayer && this.karaokeRenderer) {\n const position = this.currentPlayer.getCurrentPosition();\n this.karaokeRenderer.setCurrentTime(position);\n }\n\n if (this.karaokeRenderer) {\n this.karaokeRenderer.setPlaying(true);\n }\n\n // Play the actual audio\n if (this.currentPlayer) {\n await this.currentPlayer.play();\n }\n }\n\n async pause() {\n this.isPlaying = false;\n\n if (this.karaokeRenderer) {\n this.karaokeRenderer.setPlaying(false);\n }\n\n // Pause the actual audio\n if (this.currentPlayer) {\n await this.currentPlayer.pause();\n }\n }\n\n // Utility methods removed - no longer needed (formatTime handled by formatUtils.js)\n // debugLoadVocals removed - no longer needed\n\n applyWaveformSettings(settings) {\n // Apply settings to active renderer (KAI or CDG)\n if (this.currentFormat === 'kai' && this.karaokeRenderer) {\n // Update waveformPreferences object (used by renderer)\n if (settings.enableWaveforms !== undefined) {\n this.karaokeRenderer.waveformPreferences.enableWaveforms = settings.enableWaveforms;\n }\n if (settings.enableEffects !== undefined) {\n this.karaokeRenderer.waveformPreferences.enableEffects = settings.enableEffects;\n }\n if (settings.showUpcomingLyrics !== undefined) {\n this.karaokeRenderer.waveformPreferences.showUpcomingLyrics = settings.showUpcomingLyrics;\n }\n if (settings.overlayOpacity !== undefined) {\n this.karaokeRenderer.waveformPreferences.overlayOpacity = settings.overlayOpacity;\n }\n } else if (this.currentFormat === 'cdg' && this.cdgPlayer) {\n // CDG player settings\n if (settings.enableEffects !== undefined) {\n this.cdgPlayer.setEffectsEnabled(settings.enableEffects);\n }\n if (settings.overlayOpacity !== undefined) {\n this.cdgPlayer.overlayOpacity = settings.overlayOpacity;\n }\n }\n }\n\n destroy() {\n if (this.updateTimer) {\n clearInterval(this.updateTimer);\n }\n\n if (this.karaokeRenderer) {\n this.karaokeRenderer.destroy();\n }\n }\n}\n"],"file":"player-DVrqp7N5.js"}
@@ -0,0 +1,2 @@
1
+ async function P(e,t,r){if(e.player.currentFormat="cdg",e.player.currentPlayer=e.player.cdgPlayer,e.player.currentPlayer.onSongEnded(()=>e.handleSongEnded()),!e.kaiPlayer){console.error("💿 Audio engine not initialized"),e.updateStatus("Error: Audio engine not ready");return}const n=e.kaiPlayer.audioContexts.PA,a=e.kaiPlayer.outputNodes.PA.masterGain,o=n.createGain();o.connect(a);const c=n.createAnalyser();if(c.fftSize=2048,o.connect(c),e.player.cdgPlayer.setAudioContext(n,o,c),await e.player.cdgPlayer.loadSong(t),e.player.cdgPlayer.micEngine){const y=await window.kaiAPI.settings.get("micToSpeakers",!0),s=await window.kaiAPI.settings.get("enableMic",!0),i=await window.kaiAPI.settings.get("autoTunePreferences",{});if(e.player.cdgPlayer.micEngine.micToSpeakers=y,e.player.cdgPlayer.micEngine.enableMic=s,i.enabled!==void 0&&(e.player.cdgPlayer.micEngine.autotuneSettings.enabled=i.enabled),i.strength!==void 0&&(e.player.cdgPlayer.micEngine.autotuneSettings.strength=i.strength),i.speed!==void 0&&(e.player.cdgPlayer.micEngine.autotuneSettings.speed=i.speed),s){const f=(await window.kaiAPI.settings.get("devicePreferences",{}))?.input?.id||"default";await e.player.cdgPlayer.startMicrophoneInput(f)}}const l=await window.kaiAPI.settings.get("waveformPreferences",{enableEffects:!0,randomEffectOnSong:!1,overlayOpacity:.7});await e.player.karaokeRenderer.ensureInputDeviceSelection(),e.player.cdgPlayer.setOverlayOpacity(l.overlayOpacity),e.player.cdgPlayer.setEffectsEnabled(l.enableEffects),await g(e,t,l);const u={...r,requester:r.requester||t.requester||e.currentSong?.requester};e.player.onSongLoaded(u),window.kaiAPI?.renderer&&window.kaiAPI.renderer.songLoaded({path:e.currentSong?.originalFilePath||e.currentSong?.filePath,metadata:r,isLoading:!1,title:r?.title||"CDG Song",artist:r?.artist||"Unknown Artist",format:"cdg",duration:e.player.cdgPlayer?.getDuration()||0,requester:e.currentSong?.requester||"KJ"}),e.updateStatus(`Loaded: ${r?.title||"CDG Song"}`)}async function S(e,t,r){if(e.player.currentFormat="kai",e.player.currentPlayer=e.kaiPlayer,e.player.currentPlayer.onSongEnded(()=>e.handleSongEnded()),e.kaiPlayer&&e.currentSong){const n={...e.currentSong,audio:e.currentSong.audio?{...e.currentSong.audio,sources:e.currentSong.audio.sources?[...e.currentSong.audio.sources]:[]}:null};await e.kaiPlayer.reinitialize(),await e.kaiPlayer.loadSong(n),!e.currentSong.audio&&n.audio&&(e.currentSong.audio=n.audio),!e.currentSong.lyrics&&n.lyrics&&(e.currentSong.lyrics=n.lyrics)}if(e.player&&e.currentSong){e.player.karaokeRenderer&&e.player.karaokeRenderer.reinitialize();const n={...r,lyrics:e.currentSong.lyrics,duration:e.kaiPlayer?e.kaiPlayer.getDuration():e.currentSong.metadata?.duration||0,audio:e.currentSong.audio,requester:r.requester||t.requester||e.currentSong.requester};e.player.onSongLoaded(n);const a=await window.kaiAPI.settings.get("waveformPreferences");e.player.karaokeRenderer&&(e.player.karaokeRenderer.setWaveformsEnabled(a.enableWaveforms),e.player.karaokeRenderer.setEffectsEnabled(a.enableEffects),e.player.karaokeRenderer.setShowUpcomingLyrics(a.showUpcomingLyrics),e.player.karaokeRenderer.waveformPreferences.overlayOpacity=a.overlayOpacity,e.kaiPlayer?.outputNodes?.PA?.analyser&&e.player.karaokeRenderer.setVisualizationAnalyser(e.kaiPlayer.outputNodes.PA.analyser),a.enableWaveforms&&setTimeout(()=>{e.player.karaokeRenderer.startMicrophoneCapture()},100),setTimeout(()=>e.updateEffectDisplay(),100),await d(e,a))}await new Promise(n=>setTimeout(n,500)),e._pendingMetadata=null}async function g(e,t,r){e.player.karaokeRenderer.effectsCanvas&&e.player.karaokeRenderer.butterchurn&&(e.player.cdgPlayer.setEffectsCanvas(e.player.karaokeRenderer.effectsCanvas,e.player.karaokeRenderer.butterchurn),e.player.cdgPlayer.analyserNode&&e.player.karaokeRenderer.setVisualizationAnalyser(e.player.cdgPlayer.analyserNode),await d(e,r))}function d(e,t){t.randomEffectOnSong&&e.player.karaokeRenderer.butterchurn&&(e.randomEffectTimeout&&clearTimeout(e.randomEffectTimeout),e.randomEffectTimeout=setTimeout(async()=>{try{await window.kaiAPI.effects.random()}catch(r){console.error("Failed to apply random effect:",r)}},500))}export{P as loadCDGSong,S as loadKAISong};
2
+ //# sourceMappingURL=songLoaders-BaTgGib4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"songLoaders-BaTgGib4.js","sources":["../../js/songLoaders.js"],"sourcesContent":["/**\n * Song Loading Utilities\n * Extracted from main.js to simplify song loading logic\n */\n\n/**\n * Load CDG format song\n */\nexport async function loadCDGSong(app, songData, metadata) {\n app.player.currentFormat = 'cdg';\n app.player.currentPlayer = app.player.cdgPlayer;\n\n // Set song end callback\n app.player.currentPlayer.onSongEnded(() => app.handleSongEnded());\n\n // Set up audio context for CDG renderer (PA output only)\n if (!app.kaiPlayer) {\n console.error('💿 Audio engine not initialized');\n app.updateStatus('Error: Audio engine not ready');\n return;\n }\n\n // Get PA audio context for playback\n const paContext = app.kaiPlayer.audioContexts.PA;\n const paMasterGain = app.kaiPlayer.outputNodes.PA.masterGain;\n\n // Create gain node for CDG audio in PA context\n const cdgGainNode = paContext.createGain();\n cdgGainNode.connect(paMasterGain);\n\n // Create analyser in PA context\n const analyserNode = paContext.createAnalyser();\n analyserNode.fftSize = 2048;\n // Analyser taps the signal but doesn't affect routing\n cdgGainNode.connect(analyserNode);\n\n // Set audio context in CDG renderer (PA context for playback)\n app.player.cdgPlayer.setAudioContext(paContext, cdgGainNode, analyserNode);\n\n // Load CDG data\n await app.player.cdgPlayer.loadSong(songData);\n\n // Load microphone settings for CDG (after micEngine is initialized)\n if (app.player.cdgPlayer.micEngine) {\n const micToSpeakers = await window.kaiAPI.settings.get('micToSpeakers', true);\n const enableMic = await window.kaiAPI.settings.get('enableMic', true);\n const autoTunePrefs = await window.kaiAPI.settings.get('autoTunePreferences', {});\n\n // Apply settings to microphone engine\n app.player.cdgPlayer.micEngine.micToSpeakers = micToSpeakers;\n app.player.cdgPlayer.micEngine.enableMic = enableMic;\n\n if (autoTunePrefs.enabled !== undefined) {\n app.player.cdgPlayer.micEngine.autotuneSettings.enabled = autoTunePrefs.enabled;\n }\n if (autoTunePrefs.strength !== undefined) {\n app.player.cdgPlayer.micEngine.autotuneSettings.strength = autoTunePrefs.strength;\n }\n if (autoTunePrefs.speed !== undefined) {\n app.player.cdgPlayer.micEngine.autotuneSettings.speed = autoTunePrefs.speed;\n }\n\n // Start microphone if enabled\n if (enableMic) {\n const devicePrefs = await window.kaiAPI.settings.get('devicePreferences', {});\n const inputDeviceId = devicePrefs?.input?.id || 'default';\n await app.player.cdgPlayer.startMicrophoneInput(inputDeviceId);\n }\n }\n\n // Load and apply waveform preferences from settings for CDG\n const waveformPrefs = await window.kaiAPI.settings.get('waveformPreferences', {\n enableEffects: true,\n randomEffectOnSong: false,\n overlayOpacity: 0.7,\n });\n\n // Load device preferences for microphone\n await app.player.karaokeRenderer.ensureInputDeviceSelection();\n\n // Apply preferences to CDG player\n app.player.cdgPlayer.setOverlayOpacity(waveformPrefs.overlayOpacity);\n app.player.cdgPlayer.setEffectsEnabled(waveformPrefs.enableEffects);\n\n // Set Butterchurn effects canvas for CDG background\n await setupButterchurnForCDG(app, songData, waveformPrefs);\n\n // CDG doesn't use audio engine or lyrics\n // Include requester from songData if not in metadata\n const fullMetadata = {\n ...metadata,\n requester: metadata.requester || songData.requester || app.currentSong?.requester,\n };\n app.player.onSongLoaded(fullMetadata);\n\n // Broadcast that CDG is ready (clear loading state)\n if (window.kaiAPI?.renderer) {\n window.kaiAPI.renderer.songLoaded({\n path: app.currentSong?.originalFilePath || app.currentSong?.filePath,\n metadata: metadata,\n isLoading: false,\n title: metadata?.title || 'CDG Song',\n artist: metadata?.artist || 'Unknown Artist',\n format: 'cdg',\n duration: app.player.cdgPlayer?.getDuration() || 0,\n requester: app.currentSong?.requester || 'KJ',\n });\n }\n\n // Controls now managed by React TransportControlsWrapper\n app.updateStatus(`Loaded: ${metadata?.title || 'CDG Song'}`);\n}\n\n/**\n * Load KAI format song\n */\nexport async function loadKAISong(app, songData, metadata) {\n app.player.currentFormat = 'kai';\n app.player.currentPlayer = app.kaiPlayer;\n\n // Set song end callback\n app.player.currentPlayer.onSongEnded(() => app.handleSongEnded());\n\n // CLEAN SLATE APPROACH: Reinitialize audio engine\n if (app.kaiPlayer && app.currentSong) {\n // Create a backup copy of the song data BEFORE reinitialize\n const songDataBackup = {\n ...app.currentSong,\n audio: app.currentSong.audio\n ? {\n ...app.currentSong.audio,\n sources: app.currentSong.audio.sources ? [...app.currentSong.audio.sources] : [],\n }\n : null,\n };\n\n await app.kaiPlayer.reinitialize();\n await app.kaiPlayer.loadSong(songDataBackup);\n\n // Restore the original song data if it was corrupted\n if (!app.currentSong.audio && songDataBackup.audio) {\n app.currentSong.audio = songDataBackup.audio;\n }\n if (!app.currentSong.lyrics && songDataBackup.lyrics) {\n app.currentSong.lyrics = songDataBackup.lyrics;\n }\n }\n\n // CLEAN SLATE APPROACH: Reinitialize karaoke renderer\n if (app.player && app.currentSong) {\n if (app.player.karaokeRenderer) {\n app.player.karaokeRenderer.reinitialize();\n }\n\n // Pass full song data which includes lyrics, audio sources, and updated duration from audio engine\n const fullMetadata = {\n ...metadata,\n lyrics: app.currentSong.lyrics,\n duration: app.kaiPlayer\n ? app.kaiPlayer.getDuration()\n : app.currentSong.metadata?.duration || 0,\n audio: app.currentSong.audio, // Include audio sources for vocals waveform\n requester: metadata.requester || songData.requester || app.currentSong.requester,\n };\n app.player.onSongLoaded(fullMetadata);\n\n // Load and apply waveform preferences from settings for KAI\n // Defaults are now provided by settingsManager from shared/defaults.js\n const waveformPrefs = await window.kaiAPI.settings.get('waveformPreferences');\n\n // Apply preferences to karaokeRenderer\n if (app.player.karaokeRenderer) {\n app.player.karaokeRenderer.setWaveformsEnabled(waveformPrefs.enableWaveforms);\n app.player.karaokeRenderer.setEffectsEnabled(waveformPrefs.enableEffects);\n app.player.karaokeRenderer.setShowUpcomingLyrics(waveformPrefs.showUpcomingLyrics);\n app.player.karaokeRenderer.waveformPreferences.overlayOpacity = waveformPrefs.overlayOpacity;\n\n // Connect butterchurn to PA analyser for visualization (KAI format)\n if (app.kaiPlayer?.outputNodes?.PA?.analyser) {\n app.player.karaokeRenderer.setVisualizationAnalyser(app.kaiPlayer.outputNodes.PA.analyser);\n }\n\n // Restart microphone capture if waveforms are enabled\n if (waveformPrefs.enableWaveforms) {\n setTimeout(() => {\n app.player.karaokeRenderer.startMicrophoneCapture();\n }, 100);\n }\n\n // Update effect display with current preset\n setTimeout(() => app.updateEffectDisplay(), 100);\n\n // Apply random effect if enabled\n await applyRandomEffectIfEnabled(app, waveformPrefs);\n }\n }\n\n // Wait for all contexts and buffers to be ready\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n // Clear pending metadata\n app._pendingMetadata = null;\n}\n\n/**\n * Setup Butterchurn for CDG visualization\n */\nasync function setupButterchurnForCDG(app, songData, waveformPrefs) {\n if (app.player.karaokeRenderer.effectsCanvas && app.player.karaokeRenderer.butterchurn) {\n app.player.cdgPlayer.setEffectsCanvas(\n app.player.karaokeRenderer.effectsCanvas,\n app.player.karaokeRenderer.butterchurn\n );\n\n // Connect butterchurn to CDG's PA analyser (already connected to CDG audio source)\n if (app.player.cdgPlayer.analyserNode) {\n app.player.karaokeRenderer.setVisualizationAnalyser(app.player.cdgPlayer.analyserNode);\n }\n\n // Apply random effect if enabled\n await applyRandomEffectIfEnabled(app, waveformPrefs);\n }\n}\n\n/**\n * Load M4A Stems format song\n */\nexport async function loadM4ASong(app, songData, metadata) {\n app.player.currentFormat = 'm4a-stems';\n app.player.currentPlayer = app.kaiPlayer;\n\n // Set song end callback\n app.player.currentPlayer.onSongEnded(() => app.handleSongEnded());\n\n // CLEAN SLATE APPROACH: Reinitialize audio engine (same as KAI)\n if (app.kaiPlayer && app.currentSong) {\n // Create a backup copy of the song data BEFORE reinitialize\n const songDataBackup = {\n ...app.currentSong,\n audio: app.currentSong.audio\n ? {\n ...app.currentSong.audio,\n sources: app.currentSong.audio.sources ? [...app.currentSong.audio.sources] : [],\n }\n : null,\n };\n\n await app.kaiPlayer.reinitialize();\n await app.kaiPlayer.loadSong(songDataBackup);\n\n // Restore the original song data if it was corrupted\n if (!app.currentSong.audio && songDataBackup.audio) {\n app.currentSong.audio = songDataBackup.audio;\n }\n if (!app.currentSong.lyrics && songDataBackup.lyrics) {\n app.currentSong.lyrics = songDataBackup.lyrics;\n }\n }\n\n // CLEAN SLATE APPROACH: Reinitialize karaoke renderer\n if (app.player && app.currentSong) {\n if (app.player.karaokeRenderer) {\n app.player.karaokeRenderer.reinitialize();\n }\n\n // Pass full song data which includes lyrics, audio sources, and updated duration\n const fullMetadata = {\n ...metadata,\n lyrics: app.currentSong.lyrics,\n duration: app.kaiPlayer\n ? app.kaiPlayer.getDuration()\n : app.currentSong.metadata?.duration || 0,\n audio: app.currentSong.audio, // Include audio sources for vocals waveform\n requester: metadata.requester || songData.requester || app.currentSong.requester,\n };\n app.player.onSongLoaded(fullMetadata);\n\n // Load and apply waveform preferences from settings\n // Defaults are now provided by settingsManager from shared/defaults.js\n const waveformPrefs = await window.kaiAPI.settings.get('waveformPreferences');\n\n // Apply preferences to karaokeRenderer\n if (app.player.karaokeRenderer) {\n app.player.karaokeRenderer.setWaveformsEnabled(waveformPrefs.enableWaveforms);\n app.player.karaokeRenderer.setEffectsEnabled(waveformPrefs.enableEffects);\n app.player.karaokeRenderer.setShowUpcomingLyrics(waveformPrefs.showUpcomingLyrics);\n app.player.karaokeRenderer.waveformPreferences.overlayOpacity = waveformPrefs.overlayOpacity;\n\n // Connect butterchurn to PA analyser for visualization (M4A format)\n if (app.kaiPlayer?.outputNodes?.PA?.analyser) {\n app.player.karaokeRenderer.setVisualizationAnalyser(app.kaiPlayer.outputNodes.PA.analyser);\n }\n\n // Restart microphone capture if waveforms are enabled\n if (waveformPrefs.enableWaveforms) {\n setTimeout(() => {\n app.player.karaokeRenderer.startMicrophoneCapture();\n }, 100);\n }\n\n // Update effect display with current preset\n setTimeout(() => app.updateEffectDisplay(), 100);\n\n // Apply random effect if enabled\n await applyRandomEffectIfEnabled(app, waveformPrefs);\n }\n }\n\n // Wait for all contexts and buffers to be ready\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n // Clear pending metadata\n app._pendingMetadata = null;\n}\n\n/**\n * Apply random Butterchurn effect if enabled (with debouncing)\n */\nfunction applyRandomEffectIfEnabled(app, waveformPrefs) {\n if (waveformPrefs.randomEffectOnSong && app.player.karaokeRenderer.butterchurn) {\n // Clear any existing timeout\n if (app.randomEffectTimeout) {\n clearTimeout(app.randomEffectTimeout);\n }\n\n app.randomEffectTimeout = setTimeout(async () => {\n try {\n await window.kaiAPI.effects.random();\n } catch (error) {\n console.error('Failed to apply random effect:', error);\n }\n }, 500);\n }\n}\n"],"names":["loadCDGSong","app","songData","metadata","paContext","paMasterGain","cdgGainNode","analyserNode","micToSpeakers","enableMic","autoTunePrefs","inputDeviceId","waveformPrefs","setupButterchurnForCDG","fullMetadata","loadKAISong","songDataBackup","applyRandomEffectIfEnabled","resolve","error"],"mappings":"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"}