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,257 @@
1
+ /**
2
+ * Unified Settings Service
3
+ * Single entry point for all settings operations
4
+ * Handles persistence, AppState sync, and broadcasting
5
+ */
6
+
7
+ import { ALL_DEFAULTS, mergeWithDefaults } from '../defaults.js';
8
+
9
+ let _settingsManager = null;
10
+ let _appState = null;
11
+ let _broadcastFn = null;
12
+ let _initialized = false;
13
+
14
+ /**
15
+ * Initialize the service with dependencies (called once from main.js)
16
+ * @param {Object} settingsManager - SettingsManager instance
17
+ * @param {Object} appState - AppState instance
18
+ * @param {Function} broadcastFn - Function to broadcast changes (key, value) => void
19
+ */
20
+ export function initSettingsService(settingsManager, appState, broadcastFn) {
21
+ _settingsManager = settingsManager;
22
+ _appState = appState;
23
+ _broadcastFn = broadcastFn;
24
+ _initialized = true;
25
+ console.log('✅ Settings service initialized');
26
+ }
27
+
28
+ /**
29
+ * Check if service is initialized
30
+ */
31
+ export function isInitialized() {
32
+ return _initialized;
33
+ }
34
+
35
+ /**
36
+ * Get a setting value (with defaults applied)
37
+ * @param {string} key - Setting key (supports dot notation like 'creator.llm')
38
+ * @param {*} defaultValue - Optional default if not found
39
+ * @returns {*} Setting value
40
+ */
41
+ export function getSetting(key, defaultValue) {
42
+ if (!_settingsManager) {
43
+ console.warn('⚠️ Settings service not initialized, using default');
44
+ return defaultValue ?? getDefaultForKey(key);
45
+ }
46
+
47
+ const saved = _settingsManager.get(key);
48
+ if (saved !== undefined) return saved;
49
+
50
+ // Fall back to ALL_DEFAULTS if no explicit default provided
51
+ if (defaultValue === undefined) {
52
+ return getDefaultForKey(key);
53
+ }
54
+ return defaultValue;
55
+ }
56
+
57
+ /**
58
+ * Get default value for a key from ALL_DEFAULTS
59
+ * Supports dot notation
60
+ */
61
+ function getDefaultForKey(key) {
62
+ if (!key) return undefined;
63
+
64
+ // Handle dot notation
65
+ const parts = key.split('.');
66
+ let value = ALL_DEFAULTS;
67
+ for (const part of parts) {
68
+ if (value && typeof value === 'object' && part in value) {
69
+ value = value[part];
70
+ } else {
71
+ return undefined;
72
+ }
73
+ }
74
+ return value;
75
+ }
76
+
77
+ /**
78
+ * Get all settings (merged with defaults)
79
+ * @returns {Object} All settings
80
+ */
81
+ export function getAllSettings() {
82
+ if (!_settingsManager) {
83
+ console.warn('⚠️ Settings service not initialized, using defaults');
84
+ return { ...ALL_DEFAULTS };
85
+ }
86
+
87
+ const saved = _settingsManager.getAll();
88
+ return mergeWithDefaults(saved);
89
+ }
90
+
91
+ /**
92
+ * Set a setting and broadcast to all clients
93
+ * @param {string} key - Setting key
94
+ * @param {*} value - Setting value
95
+ * @param {Object} options - Options
96
+ * @param {boolean} options.skipBroadcast - Don't broadcast change
97
+ * @param {boolean} options.skipPersist - Don't persist to disk
98
+ * @param {boolean} options.skipAppState - Don't sync to AppState
99
+ * @returns {Object} Result with success status
100
+ */
101
+ export function setSetting(key, value, options = {}) {
102
+ const { skipBroadcast = false, skipPersist = false, skipAppState = false } = options;
103
+
104
+ if (!_settingsManager && !skipPersist) {
105
+ console.error('❌ Settings service not initialized');
106
+ return { success: false, error: 'Settings service not initialized' };
107
+ }
108
+
109
+ // 1. Persist to disk
110
+ if (!skipPersist && _settingsManager) {
111
+ _settingsManager.set(key, value);
112
+ }
113
+
114
+ // 2. Update AppState mirror (for relevant keys)
115
+ if (!skipAppState) {
116
+ syncToAppState(key, value);
117
+ }
118
+
119
+ // 3. Broadcast to all clients (renderer + web)
120
+ if (!skipBroadcast && _broadcastFn) {
121
+ _broadcastFn(key, value);
122
+ }
123
+
124
+ return { success: true, key, value };
125
+ }
126
+
127
+ /**
128
+ * Update multiple settings at once
129
+ * @param {Object} updates - Key-value pairs to update
130
+ * @param {Object} options - Options (same as setSetting)
131
+ * @returns {Object} Result with success status
132
+ */
133
+ export async function setSettings(updates, options = {}) {
134
+ const results = {};
135
+
136
+ for (const [key, value] of Object.entries(updates)) {
137
+ const result = await setSetting(key, value, { ...options, skipBroadcast: true });
138
+ results[key] = result;
139
+ }
140
+
141
+ // Single broadcast for batch update
142
+ if (!options.skipBroadcast && _broadcastFn) {
143
+ _broadcastFn('settings:batch', updates);
144
+ }
145
+
146
+ return { success: true, updates: results };
147
+ }
148
+
149
+ /**
150
+ * Sync a setting to AppState (keeps memory mirror in sync)
151
+ * @param {string} key - Setting key
152
+ * @param {*} value - Setting value
153
+ */
154
+ function syncToAppState(key, value) {
155
+ if (!_appState) return;
156
+
157
+ // Map settings keys to AppState update methods
158
+ switch (key) {
159
+ case 'mixer':
160
+ _appState.update('mixer', value);
161
+ break;
162
+
163
+ case 'effects':
164
+ case 'waveformPreferences':
165
+ // Sync effects-related settings
166
+ if (_appState.state?.effects) {
167
+ _appState.update('effects', {
168
+ ..._appState.state.effects,
169
+ ...value,
170
+ });
171
+ }
172
+ break;
173
+
174
+ case 'autoTune':
175
+ case 'autoTunePreferences':
176
+ if (_appState.updatePreferences) {
177
+ _appState.updatePreferences('autoTune', value);
178
+ }
179
+ break;
180
+
181
+ case 'microphone':
182
+ if (_appState.updatePreferences) {
183
+ _appState.updatePreferences('microphone', value);
184
+ }
185
+ break;
186
+
187
+ case 'audioDevices':
188
+ case 'devicePreferences':
189
+ if (_appState.setAudioDevices) {
190
+ _appState.setAudioDevices(value);
191
+ }
192
+ break;
193
+
194
+ case 'iemMonoVocals':
195
+ if (_appState.updatePreferences) {
196
+ _appState.updatePreferences('iemMonoVocals', value);
197
+ }
198
+ break;
199
+
200
+ // Add more mappings as needed
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Load settings on startup and sync to AppState
206
+ * @returns {Object} Loaded settings
207
+ */
208
+ export function loadAndSync() {
209
+ const settings = getAllSettings();
210
+
211
+ // Sync each category to AppState
212
+ if (settings.mixer) syncToAppState('mixer', settings.mixer);
213
+ if (settings.effects) syncToAppState('effects', settings.effects);
214
+ if (settings.waveformPreferences)
215
+ syncToAppState('waveformPreferences', settings.waveformPreferences);
216
+ if (settings.autoTune) syncToAppState('autoTune', settings.autoTune);
217
+ if (settings.autoTunePreferences)
218
+ syncToAppState('autoTunePreferences', settings.autoTunePreferences);
219
+ if (settings.microphone) syncToAppState('microphone', settings.microphone);
220
+ if (settings.audioDevices) syncToAppState('audioDevices', settings.audioDevices);
221
+ if (settings.devicePreferences) syncToAppState('devicePreferences', settings.devicePreferences);
222
+
223
+ console.log('✅ Settings loaded and synced to AppState');
224
+ return settings;
225
+ }
226
+
227
+ /**
228
+ * Get the broadcast channel name for a settings key
229
+ * @param {string} key - Settings key
230
+ * @returns {string} Channel name
231
+ */
232
+ export function getBroadcastChannel(key) {
233
+ const channelMap = {
234
+ mixer: 'mixer:state',
235
+ effects: 'effects:changed',
236
+ waveformPreferences: 'waveform:settingsChanged',
237
+ autoTune: 'autotune:settingsChanged',
238
+ autoTunePreferences: 'autotune:settingsChanged',
239
+ microphone: 'preferences:updated',
240
+ audioDevices: 'preferences:updated',
241
+ devicePreferences: 'preferences:updated',
242
+ 'settings:batch': 'settings:updated',
243
+ };
244
+
245
+ return channelMap[key] || `settings:${key}`;
246
+ }
247
+
248
+ export default {
249
+ initSettingsService,
250
+ isInitialized,
251
+ getSetting,
252
+ getAllSettings,
253
+ setSetting,
254
+ setSettings,
255
+ loadAndSync,
256
+ getBroadcastChannel,
257
+ };
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Settings Service Tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
6
+
7
+ // We need to test the module fresh each time, so we'll use dynamic imports
8
+ // and resetModules to avoid state pollution between tests
9
+ describe('settingsService', () => {
10
+ let settingsService;
11
+ let mockSettingsManager;
12
+ let mockAppState;
13
+ let mockBroadcastFn;
14
+
15
+ beforeEach(async () => {
16
+ // Reset module state before each test
17
+ vi.resetModules();
18
+
19
+ // Create fresh mocks
20
+ mockSettingsManager = {
21
+ get: vi.fn(),
22
+ set: vi.fn(),
23
+ getAll: vi.fn(() => ({})),
24
+ };
25
+
26
+ mockAppState = {
27
+ state: {
28
+ mixer: { PA: { gain: 0 }, IEM: { gain: 0 } },
29
+ effects: { reverb: 0.5 },
30
+ },
31
+ update: vi.fn(),
32
+ updatePreferences: vi.fn(),
33
+ setAudioDevices: vi.fn(),
34
+ };
35
+
36
+ mockBroadcastFn = vi.fn();
37
+
38
+ // Import fresh module
39
+ settingsService = await import('./settingsService.js');
40
+ });
41
+
42
+ describe('initSettingsService', () => {
43
+ it('should initialize the service', () => {
44
+ expect(settingsService.isInitialized()).toBe(false);
45
+
46
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
47
+
48
+ expect(settingsService.isInitialized()).toBe(true);
49
+ });
50
+ });
51
+
52
+ describe('isInitialized', () => {
53
+ it('should return false before initialization', () => {
54
+ expect(settingsService.isInitialized()).toBe(false);
55
+ });
56
+
57
+ it('should return true after initialization', () => {
58
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
59
+ expect(settingsService.isInitialized()).toBe(true);
60
+ });
61
+ });
62
+
63
+ describe('getSetting', () => {
64
+ it('should return default value when not initialized', () => {
65
+ const result = settingsService.getSetting('someKey', 'defaultValue');
66
+ expect(result).toBe('defaultValue');
67
+ });
68
+
69
+ it('should return saved value when available', () => {
70
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
71
+ mockSettingsManager.get.mockReturnValue('savedValue');
72
+
73
+ const result = settingsService.getSetting('someKey');
74
+
75
+ expect(mockSettingsManager.get).toHaveBeenCalledWith('someKey');
76
+ expect(result).toBe('savedValue');
77
+ });
78
+
79
+ it('should return explicit default when saved value is undefined', () => {
80
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
81
+ mockSettingsManager.get.mockReturnValue(undefined);
82
+
83
+ const result = settingsService.getSetting('someKey', 'explicitDefault');
84
+
85
+ expect(result).toBe('explicitDefault');
86
+ });
87
+
88
+ it('should support dot notation for defaults lookup', () => {
89
+ // This tests the getDefaultForKey internal function
90
+ const result = settingsService.getSetting('nonexistent.nested.key', 'fallback');
91
+ expect(result).toBe('fallback');
92
+ });
93
+ });
94
+
95
+ describe('getAllSettings', () => {
96
+ it('should return defaults when not initialized', () => {
97
+ const result = settingsService.getAllSettings();
98
+ expect(result).toBeDefined();
99
+ expect(typeof result).toBe('object');
100
+ });
101
+
102
+ it('should merge saved settings with defaults', () => {
103
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
104
+ mockSettingsManager.getAll.mockReturnValue({ customKey: 'customValue' });
105
+
106
+ const result = settingsService.getAllSettings();
107
+
108
+ expect(mockSettingsManager.getAll).toHaveBeenCalled();
109
+ expect(result).toBeDefined();
110
+ });
111
+ });
112
+
113
+ describe('setSetting', () => {
114
+ it('should return error when not initialized', async () => {
115
+ const result = await settingsService.setSetting('key', 'value');
116
+
117
+ expect(result.success).toBe(false);
118
+ expect(result.error).toBe('Settings service not initialized');
119
+ });
120
+
121
+ it('should persist, sync, and broadcast by default', async () => {
122
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
123
+
124
+ const result = await settingsService.setSetting('mixer', { PA: { gain: 5 } });
125
+
126
+ expect(result.success).toBe(true);
127
+ expect(mockSettingsManager.set).toHaveBeenCalledWith('mixer', { PA: { gain: 5 } });
128
+ expect(mockBroadcastFn).toHaveBeenCalledWith('mixer', { PA: { gain: 5 } });
129
+ });
130
+
131
+ it('should skip broadcast when skipBroadcast is true', async () => {
132
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
133
+
134
+ await settingsService.setSetting('key', 'value', { skipBroadcast: true });
135
+
136
+ expect(mockBroadcastFn).not.toHaveBeenCalled();
137
+ });
138
+
139
+ it('should skip persist when skipPersist is true', async () => {
140
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
141
+
142
+ await settingsService.setSetting('key', 'value', { skipPersist: true });
143
+
144
+ expect(mockSettingsManager.set).not.toHaveBeenCalled();
145
+ });
146
+
147
+ it('should sync mixer settings to AppState', async () => {
148
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
149
+
150
+ await settingsService.setSetting('mixer', { PA: { gain: 10 } });
151
+
152
+ expect(mockAppState.update).toHaveBeenCalledWith('mixer', { PA: { gain: 10 } });
153
+ });
154
+
155
+ it('should sync effects settings to AppState', async () => {
156
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
157
+
158
+ await settingsService.setSetting('effects', { reverb: 0.8 });
159
+
160
+ expect(mockAppState.update).toHaveBeenCalled();
161
+ });
162
+
163
+ it('should sync autoTune settings via updatePreferences', async () => {
164
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
165
+
166
+ await settingsService.setSetting('autoTune', { enabled: true });
167
+
168
+ expect(mockAppState.updatePreferences).toHaveBeenCalledWith('autoTune', { enabled: true });
169
+ });
170
+
171
+ it('should sync microphone settings via updatePreferences', async () => {
172
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
173
+
174
+ await settingsService.setSetting('microphone', { gain: 0.5 });
175
+
176
+ expect(mockAppState.updatePreferences).toHaveBeenCalledWith('microphone', { gain: 0.5 });
177
+ });
178
+
179
+ it('should sync audioDevices via setAudioDevices', async () => {
180
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
181
+
182
+ await settingsService.setSetting('audioDevices', { PA: 'device1' });
183
+
184
+ expect(mockAppState.setAudioDevices).toHaveBeenCalledWith({ PA: 'device1' });
185
+ });
186
+
187
+ it('should sync iemMonoVocals via updatePreferences', async () => {
188
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
189
+
190
+ await settingsService.setSetting('iemMonoVocals', true);
191
+
192
+ expect(mockAppState.updatePreferences).toHaveBeenCalledWith('iemMonoVocals', true);
193
+ });
194
+ });
195
+
196
+ describe('setSettings', () => {
197
+ it('should update multiple settings', async () => {
198
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
199
+
200
+ const result = await settingsService.setSettings({
201
+ key1: 'value1',
202
+ key2: 'value2',
203
+ });
204
+
205
+ expect(result.success).toBe(true);
206
+ expect(mockSettingsManager.set).toHaveBeenCalledWith('key1', 'value1');
207
+ expect(mockSettingsManager.set).toHaveBeenCalledWith('key2', 'value2');
208
+ });
209
+
210
+ it('should broadcast batch update', async () => {
211
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
212
+
213
+ await settingsService.setSettings({ key1: 'value1', key2: 'value2' });
214
+
215
+ expect(mockBroadcastFn).toHaveBeenCalledWith('settings:batch', {
216
+ key1: 'value1',
217
+ key2: 'value2',
218
+ });
219
+ });
220
+
221
+ it('should skip broadcast when option is set', async () => {
222
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
223
+
224
+ await settingsService.setSettings({ key1: 'value1' }, { skipBroadcast: true });
225
+
226
+ expect(mockBroadcastFn).not.toHaveBeenCalled();
227
+ });
228
+ });
229
+
230
+ describe('loadAndSync', () => {
231
+ it('should load all settings and sync to AppState', async () => {
232
+ settingsService.initSettingsService(mockSettingsManager, mockAppState, mockBroadcastFn);
233
+ mockSettingsManager.getAll.mockReturnValue({
234
+ mixer: { PA: { gain: 5 } },
235
+ effects: { reverb: 0.3 },
236
+ });
237
+
238
+ const result = await settingsService.loadAndSync();
239
+
240
+ expect(result).toBeDefined();
241
+ expect(mockAppState.update).toHaveBeenCalled();
242
+ });
243
+ });
244
+
245
+ describe('getBroadcastChannel', () => {
246
+ it('should return correct channel for mixer', () => {
247
+ const channel = settingsService.getBroadcastChannel('mixer');
248
+ expect(channel).toBe('mixer:state');
249
+ });
250
+
251
+ it('should return correct channel for effects', () => {
252
+ const channel = settingsService.getBroadcastChannel('effects');
253
+ expect(channel).toBe('effects:changed');
254
+ });
255
+
256
+ it('should return correct channel for waveformPreferences', () => {
257
+ const channel = settingsService.getBroadcastChannel('waveformPreferences');
258
+ expect(channel).toBe('waveform:settingsChanged');
259
+ });
260
+
261
+ it('should return correct channel for autoTune', () => {
262
+ const channel = settingsService.getBroadcastChannel('autoTune');
263
+ expect(channel).toBe('autotune:settingsChanged');
264
+ });
265
+
266
+ it('should return correct channel for microphone', () => {
267
+ const channel = settingsService.getBroadcastChannel('microphone');
268
+ expect(channel).toBe('preferences:updated');
269
+ });
270
+
271
+ it('should return correct channel for settings:batch', () => {
272
+ const channel = settingsService.getBroadcastChannel('settings:batch');
273
+ expect(channel).toBe('settings:updated');
274
+ });
275
+
276
+ it('should return default channel for unknown keys', () => {
277
+ const channel = settingsService.getBroadcastChannel('unknownKey');
278
+ expect(channel).toBe('settings:unknownKey');
279
+ });
280
+ });
281
+
282
+ describe('default export', () => {
283
+ it('should export all functions', () => {
284
+ expect(settingsService.default).toBeDefined();
285
+ expect(settingsService.default.initSettingsService).toBeDefined();
286
+ expect(settingsService.default.isInitialized).toBeDefined();
287
+ expect(settingsService.default.getSetting).toBeDefined();
288
+ expect(settingsService.default.getAllSettings).toBeDefined();
289
+ expect(settingsService.default.setSetting).toBeDefined();
290
+ expect(settingsService.default.setSettings).toBeDefined();
291
+ expect(settingsService.default.loadAndSync).toBeDefined();
292
+ expect(settingsService.default.getBroadcastChannel).toBeDefined();
293
+ });
294
+ });
295
+ });