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,685 @@
1
+ // CDGraphics will be loaded from node_modules via webpack or as a global
2
+ // For now, we'll load it dynamically when needed
3
+
4
+ /* global CDGraphics */
5
+
6
+ import { PlayerInterface } from './PlayerInterface.js';
7
+ import { MicrophoneEngine } from './microphoneEngine.js';
8
+
9
+ export class CDGPlayer extends PlayerInterface {
10
+ constructor(canvasId) {
11
+ super(); // Call PlayerInterface constructor
12
+
13
+ this.canvas = document.getElementById(canvasId);
14
+
15
+ if (!this.canvas) {
16
+ console.error('CDG canvas not found:', canvasId);
17
+ return;
18
+ }
19
+
20
+ this.ctx = this.canvas.getContext('2d');
21
+ this.cdgPlayer = null;
22
+ this.cdgData = null;
23
+ // Note: this.isPlaying is inherited from PlayerInterface
24
+ this.currentTime = 0;
25
+ this.animationFrame = null;
26
+
27
+ // CDG output canvas (300x216)
28
+ this.cdgCanvas = document.createElement('canvas');
29
+ this.cdgCanvas.width = 300;
30
+ this.cdgCanvas.height = 216;
31
+ this.cdgCtx = this.cdgCanvas.getContext('2d');
32
+
33
+ // Web Audio API for MP3 playback (will be set by main.js)
34
+ this.audioContext = null;
35
+ this.audioSource = null;
36
+ this.audioBuffer = null;
37
+ this.startTime = 0;
38
+ this.pauseTime = 0;
39
+ this.gainNode = null;
40
+ this.analyserNode = null;
41
+
42
+ // Background effects (Butterchurn)
43
+ this.effectsCanvas = null;
44
+ this.butterchurn = null;
45
+ this.effectsEnabled = true;
46
+ this.overlayOpacity = 0.7; // Default, will be updated from settings
47
+
48
+ // Microphone engine (handles mic input and auto-tune)
49
+ this.micEngine = null; // Will be initialized when audio context is set
50
+
51
+ // QR code for server URL
52
+ this.qrCodeCanvas = null;
53
+ this.showQrCode = false;
54
+ this.serverUrl = null;
55
+
56
+ // Queue display
57
+ this.queueItems = [];
58
+ this.displayQueue = true;
59
+
60
+ // Note: this.stateReportInterval is inherited from PlayerInterface
61
+ }
62
+
63
+ /**
64
+ * Set server URL and generate QR code
65
+ * @param {string} url - Server URL
66
+ * @param {boolean} show - Whether to show QR code
67
+ */
68
+ async setServerQRCode(url, show) {
69
+ this.serverUrl = url;
70
+ this.showQrCode = show;
71
+
72
+ if (url && show) {
73
+ try {
74
+ // Dynamically import QR code generator
75
+ const { generateQRCodeCanvas } = await import('../utils/qrCodeGenerator.js');
76
+ this.qrCodeCanvas = await generateQRCodeCanvas(url, 150);
77
+ } catch (error) {
78
+ console.error('Error generating QR code:', error);
79
+ this.qrCodeCanvas = null;
80
+ }
81
+ } else {
82
+ this.qrCodeCanvas = null;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Set queue items and display setting
88
+ * @param {Array} queue - Array of queue items with title, artist, requester
89
+ * @param {boolean} display - Whether to display queue
90
+ */
91
+ setQueueDisplay(queue, display) {
92
+ this.queueItems = queue || [];
93
+ this.displayQueue = display !== false;
94
+ }
95
+
96
+ setOverlayOpacity(opacity) {
97
+ this.overlayOpacity = opacity;
98
+ }
99
+
100
+ /**
101
+ * Implements PlayerInterface.loadSong()
102
+ * @param {Object} songData - CDG song data
103
+ * @returns {Promise<boolean>} Success status
104
+ */
105
+ async loadSong(_songData) {
106
+ try {
107
+ this.cdgData = _songData;
108
+
109
+ // Reset position using base class method
110
+ this.resetPosition();
111
+
112
+ // Reset CDG-specific timing state
113
+ this.currentTime = 0;
114
+ this.startTime = 0;
115
+ this.pauseTime = 0;
116
+
117
+ // Load CDGraphics library dynamically
118
+ if (typeof CDGraphics === 'undefined') {
119
+ console.error('💿 CDGraphics library not loaded');
120
+ throw new Error('CDGraphics library not available');
121
+ }
122
+
123
+ // Load CDG file data - convert to ArrayBuffer first
124
+ const cdgBuffer = _songData.cdg.data;
125
+
126
+ // Convert to ArrayBuffer
127
+ let arrayBuffer;
128
+ if (cdgBuffer instanceof Uint8Array || cdgBuffer instanceof Buffer) {
129
+ // Create a new ArrayBuffer and copy data
130
+ arrayBuffer = new ArrayBuffer(cdgBuffer.length || cdgBuffer.byteLength);
131
+ const view = new Uint8Array(arrayBuffer);
132
+ view.set(cdgBuffer);
133
+ } else if (cdgBuffer.buffer instanceof ArrayBuffer) {
134
+ // It's a typed array with an ArrayBuffer
135
+ arrayBuffer = cdgBuffer.buffer.slice(
136
+ cdgBuffer.byteOffset,
137
+ cdgBuffer.byteOffset + cdgBuffer.byteLength
138
+ );
139
+ } else if (cdgBuffer instanceof ArrayBuffer) {
140
+ // It's already an ArrayBuffer
141
+ arrayBuffer = cdgBuffer;
142
+ } else {
143
+ console.error('💿 Unknown buffer type:', cdgBuffer);
144
+ throw new Error('Unknown buffer type');
145
+ }
146
+
147
+ // Initialize CDGraphics player with the ArrayBuffer
148
+ this.cdgPlayer = new CDGraphics(arrayBuffer);
149
+
150
+ // Decode MP3 audio buffer using Web Audio API
151
+ // Audio context will be set by main.js before loading
152
+ if (!this.audioContext) {
153
+ throw new Error('Audio context not set. Call setAudioContext() first.');
154
+ }
155
+
156
+ const mp3ArrayBuffer = _songData.audio.mp3.buffer.slice(
157
+ _songData.audio.mp3.byteOffset,
158
+ _songData.audio.mp3.byteOffset + _songData.audio.mp3.byteLength
159
+ );
160
+ this.audioBuffer = await this.audioContext.decodeAudioData(mp3ArrayBuffer);
161
+
162
+ return true;
163
+ } catch (error) {
164
+ console.error('💿 Failed to load CDG:', error);
165
+ return false;
166
+ }
167
+ }
168
+
169
+ play() {
170
+ if (!this.cdgPlayer || !this.audioBuffer) {
171
+ console.warn('💿 No CDG loaded');
172
+ return;
173
+ }
174
+
175
+ this.isPlaying = true;
176
+
177
+ // Stop existing source if any (and clear its onended handler)
178
+ if (this.audioSource) {
179
+ this.audioSource.onended = null; // Clear handler before stopping
180
+ try {
181
+ this.audioSource.stop();
182
+ } catch {
183
+ // Already stopped
184
+ }
185
+ this.audioSource = null;
186
+ }
187
+
188
+ // Create new audio source
189
+ this.audioSource = this.audioContext.createBufferSource();
190
+ this.audioSource.buffer = this.audioBuffer;
191
+
192
+ // Connect to gain node (which is connected to PA output)
193
+ this.audioSource.connect(this.gainNode);
194
+
195
+ // Also connect to analyser for Butterchurn
196
+ if (this.analyserNode) {
197
+ this.audioSource.connect(this.analyserNode);
198
+ }
199
+
200
+ // Connect to microphone engine for real-time music pitch detection (auto-tune)
201
+ if (this.micEngine) {
202
+ this.micEngine.connectMusicSource(this.audioSource);
203
+ }
204
+
205
+ // Handle song end - check both isPlaying AND that we've reached the end naturally
206
+ const duration = this.audioBuffer.duration;
207
+ this.audioSource.onended = () => {
208
+ const currentPos = this.getCurrentTime();
209
+ // Only treat as ended if we're near the end of the song (within 1 second)
210
+ if (this.isPlaying && currentPos >= duration - 1) {
211
+ this.handleSongEnd();
212
+ }
213
+ };
214
+
215
+ // Start playback from current position
216
+ const offset = this.pauseTime || 0;
217
+ this.audioSource.start(0, offset);
218
+ this.startTime = this.audioContext.currentTime - offset;
219
+
220
+ this.startRendering();
221
+
222
+ // Start state reporting
223
+ this.startStateReporting();
224
+
225
+ // Update microphone engine playing state
226
+ if (this.micEngine) {
227
+ this.micEngine.setPlaying(true);
228
+ }
229
+
230
+ // Report immediate state change
231
+ this.reportStateChange();
232
+ }
233
+
234
+ pause() {
235
+ this.isPlaying = false;
236
+
237
+ // Store current position before stopping
238
+ this.pauseTime = this.getCurrentTime();
239
+
240
+ // Stop state reporting
241
+ this.stopStateReporting();
242
+
243
+ // Update microphone engine playing state
244
+ if (this.micEngine) {
245
+ this.micEngine.setPlaying(false);
246
+ }
247
+
248
+ // Report paused state
249
+ this.reportStateChange();
250
+
251
+ // Stop audio source (and clear onended handler to prevent false song-end events)
252
+ if (this.audioSource) {
253
+ this.audioSource.onended = null; // Clear handler first
254
+
255
+ // Disconnect from music analysis
256
+ if (this.micEngine) {
257
+ this.micEngine.disconnectMusicSource(this.audioSource);
258
+ }
259
+
260
+ try {
261
+ this.audioSource.stop();
262
+ } catch {
263
+ // Already stopped
264
+ }
265
+ this.audioSource = null;
266
+ }
267
+
268
+ this.stopRendering();
269
+ }
270
+
271
+ seek(positionSec) {
272
+ const wasPlaying = this.isPlaying;
273
+
274
+ // Pause if playing
275
+ if (wasPlaying) {
276
+ this.pause();
277
+ }
278
+
279
+ // Set new position
280
+ this.pauseTime = positionSec;
281
+ this.currentTime = positionSec;
282
+
283
+ // Resume if it was playing
284
+ if (wasPlaying) {
285
+ this.play();
286
+ } else {
287
+ // Force a frame render at new position
288
+ this.renderFrame();
289
+ }
290
+ }
291
+
292
+ startRendering() {
293
+ if (this.animationFrame) return;
294
+
295
+ const render = () => {
296
+ if (!this.isPlaying) return;
297
+
298
+ this.renderFrame();
299
+ this.animationFrame = requestAnimationFrame(render);
300
+ };
301
+
302
+ render();
303
+ }
304
+
305
+ stopRendering() {
306
+ if (this.animationFrame) {
307
+ cancelAnimationFrame(this.animationFrame);
308
+ this.animationFrame = null;
309
+ }
310
+ }
311
+
312
+ renderFrame() {
313
+ if (!this.cdgPlayer) return;
314
+
315
+ // Get current time from Web Audio API
316
+ this.currentTime = this.getCurrentTime();
317
+
318
+ // Get CDG frame for current time
319
+ const result = this.cdgPlayer.render(this.currentTime);
320
+
321
+ if (!result || !result.imageData) {
322
+ console.warn('💿 No frame data at time:', this.currentTime);
323
+ return;
324
+ }
325
+
326
+ // Make CDG background transparent for Butterchurn to show through
327
+ const imageData = result.imageData;
328
+ const data = imageData.data;
329
+
330
+ // Get the background color from the CDG result (typically index 0 in palette)
331
+ // The backgroundColor is usually at position 0,0
332
+ const bgR = data[0];
333
+ const bgG = data[1];
334
+ const bgB = data[2];
335
+
336
+ // Store background color for overlay
337
+ this.cdgBackgroundColor = { r: bgR, g: bgG, b: bgB };
338
+
339
+ // Make all pixels matching the background color transparent
340
+ for (let i = 0; i < data.length; i += 4) {
341
+ const r = data[i];
342
+ const g = data[i + 1];
343
+ const b = data[i + 2];
344
+
345
+ // If pixel matches background color, make it transparent
346
+ if (r === bgR && g === bgG && b === bgB) {
347
+ data[i + 3] = 0; // Set alpha to 0 (transparent)
348
+ }
349
+ }
350
+
351
+ // Convert modified ImageData to canvas
352
+ this.cdgCtx.putImageData(imageData, 0, 0);
353
+
354
+ // Clear main canvas
355
+ this.ctx.fillStyle = '#000';
356
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
357
+
358
+ // Draw Butterchurn background effects if enabled
359
+ if (this.effectsEnabled && this.effectsCanvas && this.butterchurn) {
360
+ try {
361
+ this.butterchurn.render();
362
+ this.ctx.drawImage(this.effectsCanvas, 0, 0, this.canvas.width, this.canvas.height);
363
+ } catch {
364
+ // Effects rendering can fail, don't crash the whole renderer
365
+ }
366
+ }
367
+
368
+ // Draw CDG background color as semi-transparent overlay (like KAI renderer does)
369
+ // This respects the overlayOpacity setting for consistent look
370
+ const overlayOpacity = this.overlayOpacity || 0.7;
371
+ this.ctx.save();
372
+ this.ctx.globalAlpha = overlayOpacity;
373
+ this.ctx.fillStyle = `rgb(${bgR}, ${bgG}, ${bgB})`;
374
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
375
+ this.ctx.restore();
376
+
377
+ // Scale and center CDG graphics on top
378
+ // CDG is 300x216, scale 5x to 1500x1080 for 1080p
379
+ const scale = 5;
380
+ const cdgWidth = 300 * scale; // 1500
381
+ const cdgHeight = 216 * scale; // 1080
382
+
383
+ // Center horizontally in 1920px canvas (210px margins on each side)
384
+ const offsetX = (this.canvas.width - cdgWidth) / 2;
385
+ const offsetY = 0; // Fill height
386
+
387
+ // Draw scaled CDG graphics on top (text and graphics only, no background)
388
+ this.ctx.imageSmoothingEnabled = false; // Pixel-perfect scaling
389
+ this.ctx.drawImage(this.cdgCanvas, offsetX, offsetY, cdgWidth, cdgHeight);
390
+
391
+ // Draw QR code overlay if enabled
392
+ this.drawQRCodeOverlay();
393
+
394
+ // Draw queue display if enabled
395
+ this.drawQueueDisplay();
396
+ }
397
+
398
+ /**
399
+ * Draw QR code in bottom left corner (only when not playing)
400
+ */
401
+ drawQRCodeOverlay() {
402
+ // Only show when not playing
403
+ if (!this.showQrCode || !this.qrCodeCanvas || this.isPlaying) {
404
+ return;
405
+ }
406
+
407
+ const padding = 20;
408
+ const qrSize = 150;
409
+ const x = padding; // Bottom left instead of right
410
+ const y = this.canvas.height - qrSize - padding;
411
+
412
+ // Draw white background with shadow
413
+ this.ctx.save();
414
+ this.ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
415
+ this.ctx.shadowBlur = 10;
416
+ this.ctx.shadowOffsetX = 2;
417
+ this.ctx.shadowOffsetY = 2;
418
+ this.ctx.fillStyle = '#FFFFFF';
419
+ this.ctx.fillRect(x - 10, y - 10, qrSize + 20, qrSize + 20);
420
+ this.ctx.restore();
421
+
422
+ // Draw QR code
423
+ this.ctx.drawImage(this.qrCodeCanvas, x, y, qrSize, qrSize);
424
+ }
425
+
426
+ /**
427
+ * Draw queue display in bottom right corner (only when not playing)
428
+ */
429
+ drawQueueDisplay() {
430
+ // Only show when setting is enabled and queue has items
431
+ if (!this.displayQueue || !this.queueItems || this.queueItems.length === 0 || this.isPlaying) {
432
+ return;
433
+ }
434
+
435
+ const width = this.canvas.width;
436
+ const height = this.canvas.height;
437
+ const padding = 120; // Move further from edge (left)
438
+ const bottomPadding = 80; // Move up from bottom
439
+ const rightX = width - padding;
440
+ const lineHeight = 64;
441
+ const labelFontSize = 48;
442
+ const songFontSize = 40;
443
+
444
+ this.ctx.save();
445
+
446
+ // Calculate text dimensions for background
447
+ this.ctx.font = `bold ${labelFontSize}px sans-serif`;
448
+ const labelText = 'Next up:';
449
+ const labelWidth = this.ctx.measureText(labelText).width;
450
+
451
+ // Measure all song texts and prepare data
452
+ let maxWidth = labelWidth;
453
+ const songData = this.queueItems.slice(0, 3).map((item) => {
454
+ const title = item.title || item.song?.title || 'Unknown';
455
+ const singer = item.requester || item.singer || '';
456
+
457
+ // Measure title
458
+ this.ctx.font = `${songFontSize}px sans-serif`;
459
+ const titleWidth = this.ctx.measureText(title).width;
460
+
461
+ // Measure singer if present
462
+ let singerWidth = 0;
463
+ if (singer) {
464
+ const singerText = ` - ${singer}`;
465
+ singerWidth = this.ctx.measureText(singerText).width;
466
+ }
467
+
468
+ const totalWidth = titleWidth + singerWidth;
469
+ maxWidth = Math.max(maxWidth, totalWidth);
470
+
471
+ return { title, singer };
472
+ });
473
+
474
+ // Calculate background dimensions
475
+ const bgWidth = maxWidth + 30;
476
+ const bgHeight = lineHeight + songData.length * lineHeight + 20;
477
+ const bgX = rightX - bgWidth;
478
+ const bgY = height - bgHeight - bottomPadding;
479
+
480
+ // Draw semi-transparent background with shadow and rounded corners
481
+ this.ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
482
+ this.ctx.shadowBlur = 10;
483
+ this.ctx.shadowOffsetX = 2;
484
+ this.ctx.shadowOffsetY = 2;
485
+ this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
486
+
487
+ // Draw rounded rectangle
488
+ const radius = 10;
489
+ this.ctx.beginPath();
490
+ this.ctx.moveTo(bgX + radius, bgY);
491
+ this.ctx.lineTo(bgX + bgWidth - radius, bgY);
492
+ this.ctx.quadraticCurveTo(bgX + bgWidth, bgY, bgX + bgWidth, bgY + radius);
493
+ this.ctx.lineTo(bgX + bgWidth, bgY + bgHeight - radius);
494
+ this.ctx.quadraticCurveTo(
495
+ bgX + bgWidth,
496
+ bgY + bgHeight,
497
+ bgX + bgWidth - radius,
498
+ bgY + bgHeight
499
+ );
500
+ this.ctx.lineTo(bgX + radius, bgY + bgHeight);
501
+ this.ctx.quadraticCurveTo(bgX, bgY + bgHeight, bgX, bgY + bgHeight - radius);
502
+ this.ctx.lineTo(bgX, bgY + radius);
503
+ this.ctx.quadraticCurveTo(bgX, bgY, bgX + radius, bgY);
504
+ this.ctx.closePath();
505
+ this.ctx.fill();
506
+
507
+ this.ctx.shadowColor = 'transparent';
508
+
509
+ // Draw "Next up:" label in blue
510
+ this.ctx.font = `bold ${labelFontSize}px sans-serif`;
511
+ this.ctx.fillStyle = '#3B82F6'; // Tailwind blue-600
512
+ this.ctx.textAlign = 'left';
513
+ this.ctx.fillText(labelText, bgX + 15, bgY + labelFontSize + 10);
514
+
515
+ // Draw queue items
516
+ this.ctx.font = `${songFontSize}px sans-serif`;
517
+ songData.forEach((item, index) => {
518
+ const textY = bgY + labelFontSize + 10 + (index + 1) * lineHeight;
519
+ const textX = bgX + 15;
520
+
521
+ // Draw title in white
522
+ this.ctx.fillStyle = '#FFFFFF';
523
+ this.ctx.fillText(item.title, textX, textY);
524
+
525
+ // Draw singer in yellow if present and not "KJ"
526
+ if (item.singer) {
527
+ const titleWidth = this.ctx.measureText(item.title).width;
528
+ const isKJ = item.singer.toUpperCase() === 'KJ';
529
+ this.ctx.fillStyle = isKJ ? '#FFFFFF' : '#FCD34D'; // yellow-300 for non-KJ singers
530
+ this.ctx.fillText(` - ${item.singer}`, textX + titleWidth, textY);
531
+ }
532
+ });
533
+
534
+ this.ctx.restore();
535
+ }
536
+
537
+ handleSongEnd() {
538
+ this.stopRendering();
539
+
540
+ // Use base class method for consistent song end handling
541
+ this._triggerSongEnd();
542
+
543
+ // Notify main process (for backward compatibility)
544
+ if (window.electronAPI && window.electronAPI.queue) {
545
+ window.electronAPI.queue.notifyComplete();
546
+ }
547
+ }
548
+
549
+ setEffectsCanvas(canvas, butterchurn) {
550
+ this.effectsCanvas = canvas;
551
+ this.butterchurn = butterchurn;
552
+ }
553
+
554
+ setEffectsEnabled(enabled) {
555
+ this.effectsEnabled = enabled;
556
+ }
557
+
558
+ getCurrentTime() {
559
+ if (this.isPlaying && this.audioContext) {
560
+ return this.audioContext.currentTime - this.startTime;
561
+ }
562
+ return this.pauseTime || 0;
563
+ }
564
+
565
+ /**
566
+ * Implements PlayerInterface method - alias for getCurrentTime()
567
+ * @returns {number} Current position in seconds
568
+ */
569
+ getCurrentPosition() {
570
+ return this.getCurrentTime();
571
+ }
572
+
573
+ getDuration() {
574
+ return this.audioBuffer ? this.audioBuffer.duration : 0;
575
+ }
576
+
577
+ /**
578
+ * Note: reportStateChange(), startStateReporting(), and stopStateReporting()
579
+ * are inherited from PlayerInterface base class
580
+ */
581
+
582
+ async setAudioContext(audioContext, gainNode, analyserNode) {
583
+ this.audioContext = audioContext;
584
+ this.gainNode = gainNode;
585
+ this.analyserNode = analyserNode;
586
+
587
+ // Initialize microphone engine with PA context
588
+ this.micEngine = new MicrophoneEngine(audioContext, gainNode, {
589
+ getCurrentPosition: () => this.getCurrentPosition(),
590
+ });
591
+
592
+ // Load auto-tune worklets
593
+ await this.micEngine.loadAutoTuneWorklet();
594
+ }
595
+
596
+ async loadAutoTuneWorklet() {
597
+ if (this.micEngine) {
598
+ await this.micEngine.loadAutoTuneWorklet();
599
+ }
600
+ }
601
+
602
+ async startMicrophoneInput(deviceId = 'default') {
603
+ if (this.micEngine) {
604
+ await this.micEngine.startMicrophoneInput(deviceId);
605
+ }
606
+ }
607
+
608
+ enableAutoTune() {
609
+ if (this.micEngine) {
610
+ this.micEngine.enableAutoTune();
611
+ }
612
+ }
613
+
614
+ disableAutoTune() {
615
+ if (this.micEngine) {
616
+ this.micEngine.disableAutoTune();
617
+ }
618
+ }
619
+
620
+ setAutoTuneSettings(settings) {
621
+ if (this.micEngine) {
622
+ this.micEngine.setAutoTuneSettings(settings);
623
+ }
624
+ }
625
+
626
+ stopMicrophoneInput() {
627
+ if (this.micEngine) {
628
+ this.micEngine.stopMicrophoneInput();
629
+ }
630
+ }
631
+
632
+ setMicToSpeakers(enabled) {
633
+ if (this.micEngine) {
634
+ this.micEngine.setMicToSpeakers(enabled);
635
+ }
636
+ }
637
+
638
+ async setEnableMic(enabled) {
639
+ if (this.micEngine) {
640
+ await this.micEngine.setEnableMic(enabled);
641
+ }
642
+ }
643
+
644
+ setMicrophoneGain(gainValue) {
645
+ if (this.micEngine) {
646
+ this.micEngine.setMicrophoneGain(gainValue);
647
+ }
648
+ }
649
+
650
+ destroy() {
651
+ super.destroy(); // Call parent cleanup (stops state reporting)
652
+
653
+ // Stop microphone engine
654
+ if (this.micEngine) {
655
+ this.micEngine.stopMicrophoneInput();
656
+ this.micEngine = null;
657
+ }
658
+
659
+ this.stopRendering();
660
+ if (this.audioSource) {
661
+ // Disconnect from music analysis
662
+ if (this.micEngine) {
663
+ this.micEngine.disconnectMusicSource(this.audioSource);
664
+ }
665
+
666
+ try {
667
+ this.audioSource.stop();
668
+ } catch {
669
+ // Already stopped
670
+ }
671
+ this.audioSource = null;
672
+ }
673
+ this.audioBuffer = null;
674
+ this.cdgPlayer = null;
675
+ this.cdgData = null;
676
+ }
677
+
678
+ /**
679
+ * Get the format type this player handles
680
+ * @returns {string} Format name
681
+ */
682
+ getFormat() {
683
+ return 'cdg';
684
+ }
685
+ }