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,677 @@
1
+ /**
2
+ * ElectronBridge - Electron-specific implementation of BridgeInterface
3
+ *
4
+ * Wraps window.kaiAPI (IPC interface exposed by preload script)
5
+ * This is the ONLY place in React code that touches window.kaiAPI
6
+ *
7
+ * Components use this bridge and never directly access window.*
8
+ */
9
+
10
+ import { BridgeInterface } from '../../shared/adapters/BridgeInterface.js';
11
+ import { WAVEFORM_DEFAULTS, AUTOTUNE_DEFAULTS } from '../../shared/defaults.js';
12
+
13
+ let _instance = null;
14
+
15
+ export class ElectronBridge extends BridgeInterface {
16
+ constructor() {
17
+ if (_instance) {
18
+ return _instance;
19
+ }
20
+ super();
21
+ this.api = window.kaiAPI;
22
+
23
+ _instance = this;
24
+ }
25
+
26
+ static getInstance() {
27
+ if (!_instance) {
28
+ _instance = new ElectronBridge();
29
+ }
30
+ return _instance;
31
+ }
32
+
33
+ // ===== Player Controls =====
34
+ // NOTE: Play/pause/seek handled via React hooks (usePlayer)
35
+
36
+ async getPlaybackState() {
37
+ const state = await this.api.app.getState();
38
+ return state.playback;
39
+ }
40
+
41
+ // ===== Queue Management =====
42
+
43
+ async getQueue() {
44
+ const state = await this.api.app.getState();
45
+ return { queue: state.queue, currentSong: state.currentSong };
46
+ }
47
+
48
+ async addToQueue(song) {
49
+ return await this.api.queue.addSong(song);
50
+ }
51
+
52
+ async removeFromQueue(id) {
53
+ return await this.api.queue.removeSong(id);
54
+ }
55
+
56
+ async clearQueue() {
57
+ return await this.api.queue.clear();
58
+ }
59
+
60
+ async reorderQueue(songId, newIndex) {
61
+ return await this.api.queue.reorderQueue(songId, newIndex);
62
+ }
63
+
64
+ async playNext() {
65
+ return await this.api.player.next();
66
+ }
67
+
68
+ async playFromQueue(songId) {
69
+ // Load song from queue by ID (uses queue service)
70
+ return await this.api.queue.load(songId);
71
+ }
72
+
73
+ // ===== Mixer Controls =====
74
+
75
+ async getMixerState() {
76
+ const state = await this.api.app.getState();
77
+ return state.mixer;
78
+ }
79
+
80
+ setMasterGain(bus, gainDb) {
81
+ // Apply locally - kaiPlayer will report back to main process via reportMixerState()
82
+ const kaiPlayer = window.app?.player?.kaiPlayer;
83
+ if (kaiPlayer) {
84
+ return kaiPlayer.setMasterGain(bus, gainDb);
85
+ }
86
+ return { success: false, error: 'Audio engine not initialized' };
87
+ }
88
+
89
+ toggleMasterMute(bus) {
90
+ // Apply locally - kaiPlayer will report back to main process via reportMixerState()
91
+ const kaiPlayer = window.app?.player?.kaiPlayer;
92
+ if (kaiPlayer) {
93
+ return kaiPlayer.toggleMasterMute(bus);
94
+ }
95
+ return { success: false, error: 'Audio engine not initialized' };
96
+ }
97
+
98
+ setMasterMute(bus, muted) {
99
+ // Apply locally - kaiPlayer will report back to main process via reportMixerState()
100
+ const kaiPlayer = window.app?.player?.kaiPlayer;
101
+ if (kaiPlayer) {
102
+ return kaiPlayer.setMasterMute(bus, muted);
103
+ }
104
+ return { success: false, error: 'Audio engine not initialized' };
105
+ }
106
+
107
+ async toggleMute(stemId, bus) {
108
+ return await this.api.mixer.toggleMute(stemId, bus);
109
+ }
110
+
111
+ // ===== Audio Device Management =====
112
+
113
+ async getAudioDevices() {
114
+ // Enumerate devices directly in renderer (main process doesn't have access to navigator.mediaDevices)
115
+ try {
116
+ if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
117
+ console.warn('MediaDevices API not available');
118
+ return [];
119
+ }
120
+
121
+ // Request permission first to get device labels
122
+ await navigator.mediaDevices
123
+ .getUserMedia({ audio: true })
124
+ .then((stream) => {
125
+ stream.getTracks().forEach((track) => track.stop());
126
+ })
127
+ .catch((err) => {
128
+ console.warn('Microphone permission denied:', err);
129
+ });
130
+
131
+ const devices = await navigator.mediaDevices.enumerateDevices();
132
+ const audioDevices = [];
133
+
134
+ devices.forEach((device, index) => {
135
+ if (device.kind === 'audiooutput' || device.kind === 'audioinput') {
136
+ audioDevices.push({
137
+ id: device.deviceId,
138
+ deviceId: device.deviceId,
139
+ label:
140
+ device.label ||
141
+ `${device.kind === 'audiooutput' ? 'Speaker' : 'Microphone'} ${index + 1}`,
142
+ name:
143
+ device.label ||
144
+ `${device.kind === 'audiooutput' ? 'Speaker' : 'Microphone'} ${index + 1}`,
145
+ maxInputChannels: device.kind === 'audioinput' ? 2 : 0,
146
+ maxOutputChannels: device.kind === 'audiooutput' ? 2 : 0,
147
+ defaultSampleRate: 48000,
148
+ hostApi: 'Web Audio API',
149
+ deviceKind: device.kind,
150
+ groupId: device.groupId,
151
+ });
152
+ }
153
+ });
154
+
155
+ return audioDevices;
156
+ } catch (error) {
157
+ console.error('Failed to enumerate audio devices:', error);
158
+ return [];
159
+ }
160
+ }
161
+
162
+ async setAudioDevice(deviceType, deviceId) {
163
+ console.log(`🎧 Setting ${deviceType} device to:`, deviceId);
164
+
165
+ // Notify main process (for persistence)
166
+ await this.api.audio.setDevice(deviceType, deviceId);
167
+
168
+ // Apply to kaiPlayer immediately (if it exists)
169
+ const kaiPlayer = window.app?.player?.kaiPlayer;
170
+ if (kaiPlayer && (deviceType === 'PA' || deviceType === 'IEM')) {
171
+ try {
172
+ await kaiPlayer.setOutputDevice(deviceType, deviceId);
173
+ console.log(`✅ Applied ${deviceType} device change to kaiPlayer`);
174
+ } catch (error) {
175
+ console.error(`❌ Failed to apply ${deviceType} device to kaiPlayer:`, error);
176
+ }
177
+ } else if (kaiPlayer && deviceType === 'input') {
178
+ try {
179
+ // For input device, restart microphone with new device
180
+ kaiPlayer.inputDevice = deviceId;
181
+ if (kaiPlayer.mixerState.enableMic) {
182
+ await kaiPlayer.startMicrophoneInput(deviceId);
183
+ }
184
+ console.log(`✅ Applied input device change to kaiPlayer`);
185
+ } catch (error) {
186
+ console.error(`❌ Failed to apply input device to kaiPlayer:`, error);
187
+ }
188
+ }
189
+
190
+ return { success: true };
191
+ }
192
+
193
+ async getDevicePreferences() {
194
+ return await this.api.settings.get('devicePreferences', {});
195
+ }
196
+
197
+ async saveDevicePreferences(preferences) {
198
+ return await this.api.settings.set('devicePreferences', preferences);
199
+ }
200
+
201
+ async getAudioSettings() {
202
+ const iemMonoVocals = await this.api.settings.get('iemMonoVocals', true);
203
+ const micToSpeakers = await this.api.settings.get('micToSpeakers', true);
204
+ const enableMic = await this.api.settings.get('enableMic', true);
205
+ return { iemMonoVocals, micToSpeakers, enableMic };
206
+ }
207
+
208
+ async saveAudioSettings(settings) {
209
+ // Save to disk
210
+ if (settings.iemMonoVocals !== undefined) {
211
+ await this.api.settings.set('iemMonoVocals', settings.iemMonoVocals);
212
+ }
213
+ if (settings.micToSpeakers !== undefined) {
214
+ await this.api.settings.set('micToSpeakers', settings.micToSpeakers);
215
+ }
216
+ if (settings.enableMic !== undefined) {
217
+ await this.api.settings.set('enableMic', settings.enableMic);
218
+ }
219
+
220
+ // Apply settings to kaiPlayer in real-time (if it exists)
221
+ const kaiPlayer = window.app?.player?.kaiPlayer;
222
+ if (kaiPlayer) {
223
+ if (settings.iemMonoVocals !== undefined) {
224
+ kaiPlayer.setIEMMonoVocals(settings.iemMonoVocals);
225
+ console.log('🎧 Applied IEM mono vocals:', settings.iemMonoVocals);
226
+ }
227
+ if (settings.micToSpeakers !== undefined) {
228
+ kaiPlayer.setMicToSpeakers(settings.micToSpeakers);
229
+ console.log('🎧 Applied mic to speakers:', settings.micToSpeakers);
230
+ }
231
+ if (settings.enableMic !== undefined) {
232
+ kaiPlayer.setEnableMic(settings.enableMic);
233
+ console.log('🎧 Applied enable mic:', settings.enableMic);
234
+ }
235
+ }
236
+
237
+ console.log('✅ Audio settings saved and applied:', settings);
238
+ }
239
+
240
+ // ===== Effects Controls =====
241
+
242
+ async getEffects() {
243
+ return await this.api.effects.getList();
244
+ }
245
+
246
+ async selectEffect(effectName) {
247
+ return await this.api.effects.select(effectName);
248
+ }
249
+
250
+ async enableEffect(effectName) {
251
+ return await this.api.effects.toggle(effectName, true);
252
+ }
253
+
254
+ async disableEffect(effectName) {
255
+ return await this.api.effects.toggle(effectName, false);
256
+ }
257
+
258
+ async toggleEffect(effectName, enabled) {
259
+ return await this.api.effects.toggle(effectName, enabled);
260
+ }
261
+
262
+ async nextEffect() {
263
+ return await this.api.effects.next();
264
+ }
265
+
266
+ async previousEffect() {
267
+ return await this.api.effects.previous();
268
+ }
269
+
270
+ async randomEffect() {
271
+ return await this.api.effects.random();
272
+ }
273
+
274
+ // ===== Library Management =====
275
+
276
+ async getLibrary() {
277
+ return await this.api.library.getSongs();
278
+ }
279
+
280
+ async scanLibrary() {
281
+ return await this.api.library.scanFolder();
282
+ }
283
+
284
+ async searchSongs(query) {
285
+ return await this.api.library.search(query);
286
+ }
287
+
288
+ async getSongsFolder() {
289
+ const result = await this.api.library.getSongsFolder();
290
+ return result.folder;
291
+ }
292
+
293
+ async setSongsFolder() {
294
+ const result = await this.api.library.setSongsFolder();
295
+ return result.folder;
296
+ }
297
+
298
+ async getCachedLibrary() {
299
+ const result = await this.api.library.getCachedSongs();
300
+ return result;
301
+ }
302
+
303
+ async syncLibrary() {
304
+ const result = await this.api.library.syncLibrary();
305
+ return result;
306
+ }
307
+
308
+ async loadSong(path) {
309
+ return await this.api.file.loadKaiFromPath(path);
310
+ }
311
+
312
+ // ===== Song Editor =====
313
+
314
+ async loadSongForEditing(path) {
315
+ // Load the KAI file for editing (using editor.loadKai which doesn't affect playback)
316
+ const result = await this.api.editor.loadKai(path);
317
+ if (!result.success) {
318
+ return { success: false, error: result.error };
319
+ }
320
+
321
+ const songData = result.data;
322
+
323
+ // Create blob URLs for audio files (for Audio element playback)
324
+ const audioFiles =
325
+ songData.audio?.sources?.map((source) => {
326
+ // Create blob URL from audioData buffer
327
+ const blob = new Blob([source.audioData], { type: 'audio/mpeg' });
328
+ const downloadUrl = URL.createObjectURL(blob);
329
+
330
+ return {
331
+ name: source.name,
332
+ filename: source.filename,
333
+ audioData: source.audioData, // Keep raw data for waveform analysis
334
+ downloadUrl: downloadUrl, // Blob URL for Audio element
335
+ };
336
+ }) || [];
337
+
338
+ // Return in the format expected by SongEditor
339
+ return {
340
+ success: true,
341
+ data: {
342
+ format: 'kai',
343
+ metadata: songData.metadata || {},
344
+ lyrics: songData.lyrics || [],
345
+ audioFiles: audioFiles,
346
+ songJson: songData.originalSongJson || {},
347
+ },
348
+ };
349
+ }
350
+
351
+ async saveSongEdits(updates) {
352
+ const { path, metadata, lyrics, format } = updates;
353
+
354
+ if (format === 'kai') {
355
+ // Build the song object for KaiWriter
356
+ const songData = {
357
+ song: {
358
+ title: metadata.title,
359
+ artist: metadata.artist,
360
+ album: metadata.album,
361
+ year: metadata.year,
362
+ genre: metadata.genre,
363
+ key: metadata.key,
364
+ },
365
+ lyrics: lyrics,
366
+ };
367
+
368
+ // Include meta if rejections/suggestions were updated
369
+ if (metadata.rejections !== undefined || metadata.suggestions !== undefined) {
370
+ songData.meta = { corrections: {} };
371
+
372
+ if (metadata.rejections !== undefined) {
373
+ songData.meta.corrections.rejected = metadata.rejections.map((r) => ({
374
+ line: r.line_num,
375
+ start: r.start_time,
376
+ end: r.end_time,
377
+ old: r.old_text,
378
+ new: r.new_text,
379
+ reason: r.reason,
380
+ word_retention: r.retention_rate,
381
+ }));
382
+ }
383
+
384
+ if (metadata.suggestions !== undefined) {
385
+ songData.meta.corrections.missing_lines_suggested = metadata.suggestions.map((s) => ({
386
+ suggested_text: s.suggested_text,
387
+ start: s.start_time,
388
+ end: s.end_time,
389
+ confidence: s.confidence,
390
+ reason: s.reason,
391
+ pitch_activity: s.pitch_activity,
392
+ }));
393
+ }
394
+ }
395
+
396
+ const result = await this.api.editor.saveKai(songData, path);
397
+ return result;
398
+ }
399
+
400
+ return { success: false, error: 'Unsupported format' };
401
+ }
402
+
403
+ // ===== Preferences =====
404
+
405
+ async getPreferences() {
406
+ const state = await this.api.app.getState();
407
+ return state.preferences;
408
+ }
409
+
410
+ async updateAutoTunePreferences(prefs) {
411
+ return await this.api.preferences.setAutoTune(prefs);
412
+ }
413
+
414
+ async updateMicrophonePreferences(prefs) {
415
+ return await this.api.preferences.setMicrophone(prefs);
416
+ }
417
+
418
+ async updateEffectsPreferences(prefs) {
419
+ return await this.api.preferences.setEffects(prefs);
420
+ }
421
+
422
+ async getWaveformPreferences() {
423
+ return await this.api.settings.get('waveformPreferences', WAVEFORM_DEFAULTS);
424
+ }
425
+
426
+ async saveWaveformPreferences(prefs) {
427
+ // Extract only serializable values (avoid React synthetic objects)
428
+ const cleanPrefs = {
429
+ enableWaveforms: prefs.enableWaveforms,
430
+ enableEffects: prefs.enableEffects,
431
+ randomEffectOnSong: prefs.randomEffectOnSong,
432
+ showUpcomingLyrics: prefs.showUpcomingLyrics,
433
+ overlayOpacity: prefs.overlayOpacity,
434
+ };
435
+
436
+ const result = await this.api.settings.set('waveformPreferences', cleanPrefs);
437
+
438
+ // Settings saved and broadcast via IPC (applied by useAudioEngine hook)
439
+ console.log('✅ Waveform preferences saved and broadcast:', cleanPrefs);
440
+
441
+ return result;
442
+ }
443
+
444
+ async getAutotunePreferences() {
445
+ return await this.api.settings.get('autoTunePreferences', AUTOTUNE_DEFAULTS);
446
+ }
447
+
448
+ async saveAutotunePreferences(prefs) {
449
+ // Extract only serializable values (avoid React synthetic objects)
450
+ const cleanPrefs = {
451
+ enabled: prefs.enabled,
452
+ strength: prefs.strength,
453
+ speed: prefs.speed,
454
+ preferVocals: prefs.preferVocals,
455
+ };
456
+
457
+ // Save to disk
458
+ const result = await this.api.settings.set('autoTunePreferences', cleanPrefs);
459
+
460
+ // Apply settings to player in real-time (if it exists)
461
+ // This ensures mid-song adjustments work immediately
462
+ const player = window.app?.player?.kaiPlayer || window.app?.player?.cdgPlayer;
463
+ if (player) {
464
+ try {
465
+ // Apply all settings at once
466
+ await player.setAutoTuneSettings(cleanPrefs);
467
+ console.log('🎤 Applied auto-tune settings in real-time:', cleanPrefs);
468
+ } catch (error) {
469
+ console.error('❌ Failed to apply auto-tune settings to player:', error);
470
+ }
471
+ }
472
+
473
+ return result;
474
+ }
475
+
476
+ async setAutotuneEnabled(enabled) {
477
+ // Apply to player immediately
478
+ const player = window.app?.player?.kaiPlayer || window.app?.player?.cdgPlayer;
479
+ if (player) {
480
+ try {
481
+ await player.setAutoTuneSettings({ enabled });
482
+ console.log('🎤 Applied auto-tune enabled:', enabled);
483
+ } catch (error) {
484
+ console.error('❌ Failed to apply auto-tune enabled:', error);
485
+ }
486
+ }
487
+
488
+ // Also notify main process for persistence
489
+ return await this.api.autotune.setEnabled(enabled);
490
+ }
491
+
492
+ async setAutotuneSettings(settings) {
493
+ // Apply to player immediately
494
+ const player = window.app?.player?.kaiPlayer || window.app?.player?.cdgPlayer;
495
+ if (player) {
496
+ try {
497
+ await player.setAutoTuneSettings(settings);
498
+ console.log('🎤 Applied auto-tune settings:', settings);
499
+ } catch (error) {
500
+ console.error('❌ Failed to apply auto-tune settings:', error);
501
+ }
502
+ }
503
+
504
+ // Also notify main process for persistence
505
+ return await this.api.autotune.setSettings(settings);
506
+ }
507
+
508
+ // Subscribe to settings changes from external sources (e.g., web admin)
509
+ onSettingsChanged(type, callback) {
510
+ const eventMap = {
511
+ waveform: 'waveform:settingsChanged',
512
+ autotune: 'autotune:settingsChanged',
513
+ };
514
+
515
+ const eventName = eventMap[type];
516
+ if (!eventName) return () => {};
517
+
518
+ // Wrap callback to handle IPC event signature (event, settings)
519
+ const wrappedCallback = (event, settings) => {
520
+ callback(settings);
521
+ };
522
+
523
+ this.api.events.on(eventName, wrappedCallback);
524
+
525
+ return () => {
526
+ this.api.events.off?.(eventName, wrappedCallback);
527
+ };
528
+ }
529
+
530
+ // ===== Song Requests =====
531
+
532
+ async getRequests() {
533
+ return await this.api.webServer.getSongRequests();
534
+ }
535
+
536
+ async approveRequest(requestId) {
537
+ return await this.api.webServer.approveRequest(requestId);
538
+ }
539
+
540
+ async rejectRequest(requestId) {
541
+ return await this.api.webServer.rejectRequest(requestId);
542
+ }
543
+
544
+ // ===== Server Management =====
545
+
546
+ async getServerUrl() {
547
+ return await this.api.webServer.getUrl();
548
+ }
549
+
550
+ async getServerSettings() {
551
+ return await this.api.webServer.getSettings();
552
+ }
553
+
554
+ async updateServerSettings(settings) {
555
+ return await this.api.webServer.updateSettings(settings);
556
+ }
557
+
558
+ async getAdminPasswordStatus() {
559
+ return await this.api.settings.get('server.adminPasswordHash');
560
+ }
561
+
562
+ async setAdminPassword(password) {
563
+ return await this.api.webServer.setAdminPassword(password);
564
+ }
565
+
566
+ async clearAllRequests() {
567
+ return await this.api.webServer.clearAllRequests();
568
+ }
569
+
570
+ // ===== System =====
571
+
572
+ async openExternal(url) {
573
+ return await this.api.shell.openExternal(url);
574
+ }
575
+
576
+ // ===== Audio Monitoring =====
577
+
578
+ onLatencyUpdate(callback) {
579
+ const handler = (event, latencyMs) => callback(latencyMs);
580
+ this.api.audio.onLatencyUpdate(handler);
581
+ return () => this.api.audio.removeLatencyListener?.(handler);
582
+ }
583
+
584
+ onXRunUpdate(callback) {
585
+ const handler = (event, count) => callback(count);
586
+ this.api.audio.onXRun(handler);
587
+ return () => this.api.audio.removeXRunListener?.(handler);
588
+ }
589
+
590
+ // ===== State Subscriptions =====
591
+
592
+ onPlaybackStateChanged(callback) {
593
+ // Use IPC event instead of polling
594
+ const handler = (event, state) => callback(state);
595
+ this.api.player.onPlaybackState(handler);
596
+
597
+ // Return cleanup function
598
+ return () => this.api.player.removePlaybackListener(handler);
599
+ }
600
+
601
+ onCurrentSongChanged(callback) {
602
+ // Use IPC event instead of polling
603
+ const handler = (event, song) => callback(song);
604
+ this.api.song.onChanged(handler);
605
+
606
+ // Return cleanup function
607
+ return () => this.api.song.removeChangedListener(handler);
608
+ }
609
+
610
+ onQueueChanged(callback) {
611
+ // Use IPC event instead of polling
612
+ const handler = (event, queue) => {
613
+ // Get current song from app state
614
+ this.api.app.getState().then((state) => {
615
+ callback({ queue, currentSong: state.currentSong });
616
+ });
617
+ };
618
+ this.api.queue.onUpdated(handler);
619
+
620
+ // Return cleanup function
621
+ return () => this.api.queue.removeUpdatedListener(handler);
622
+ }
623
+
624
+ onMixerChanged(callback) {
625
+ // Use IPC event instead of polling
626
+ const handler = (event, mixer) => callback(mixer);
627
+ this.api.mixer.onStateChange(handler);
628
+
629
+ // Return cleanup function
630
+ return () => this.api.mixer.removeStateListener(handler);
631
+ }
632
+
633
+ onEffectChanged(callback) {
634
+ // Use IPC event instead of polling
635
+ const handler = (event, effects) => callback(effects);
636
+ this.api.effects.onChanged(handler);
637
+
638
+ // Return cleanup function
639
+ return () => this.api.effects.removeChangedListener(handler);
640
+ }
641
+
642
+ onStateChange(domain, callback) {
643
+ // Use the specific on* methods for each domain
644
+ switch (domain) {
645
+ case 'playback':
646
+ return this.onPlaybackStateChanged(callback);
647
+ case 'mixer':
648
+ return this.onMixerChanged(callback);
649
+ case 'queue':
650
+ return this.onQueueChanged(callback);
651
+ case 'effects':
652
+ return this.onEffectChanged(callback);
653
+ case 'preferences': {
654
+ const handler = (event, prefs) => callback(prefs);
655
+ this.api.preferences.onUpdated(handler);
656
+ return () => this.api.preferences.removeUpdatedListener(handler);
657
+ }
658
+ default:
659
+ console.warn(`No state change handler for domain: ${domain}`);
660
+ return () => {};
661
+ }
662
+ }
663
+
664
+ // ===== Lifecycle =====
665
+
666
+ connect() {
667
+ // Already connected via IPC - nothing to do
668
+ console.log('✅ ElectronBridge connected');
669
+ return Promise.resolve();
670
+ }
671
+
672
+ disconnect() {
673
+ // Cleanup is now handled by the cleanup functions returned from each subscription
674
+ // Components should call the cleanup functions when they unmount
675
+ console.log('✅ ElectronBridge disconnected');
676
+ }
677
+ }