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,491 @@
1
+ /**
2
+ * Stem Builder - Creates .stem.m4a files with embedded stem data
3
+ *
4
+ * The .stem.m4a format embeds multiple audio stems in a single M4A container
5
+ * using custom atoms/boxes. This is compatible with Native Instruments Stems.
6
+ *
7
+ * Structure:
8
+ * - ftyp (file type)
9
+ * - moov (movie header with metadata)
10
+ * - udta/stem (NI Stems metadata for DJ software)
11
+ * - udta/meta/ilst/kara (karaoke data: lyrics, timing, word-level timing)
12
+ * - mdat (media data with stems)
13
+ *
14
+ * Note: CREPE pitch detection is used only for key detection during creation.
15
+ * Vocal pitch tracking for auto-tune/scoring is done at runtime.
16
+ */
17
+
18
+ import { spawn } from 'child_process';
19
+ import { getFFmpegPath } from './systemChecker.js';
20
+ import { Atoms as M4AAtoms } from 'm4a-stems';
21
+
22
+ /**
23
+ * Build a .stem.m4a file from individual stem files
24
+ *
25
+ * @param {Object} options - Build options
26
+ * @param {string} options.outputPath - Output .stem.m4a path
27
+ * @param {Object} options.stems - Map of stem name to path
28
+ * @param {Object} options.metadata - Song metadata (title, artist, duration)
29
+ * @param {Object} options.lyrics - Whisper transcription result with word timestamps
30
+ * @param {Object} options.pitch - CREPE pitch detection result
31
+ * @param {string[]} options.tags - Tags array for filtering (e.g., ['ai_corrected'])
32
+ * @returns {Promise<void>}
33
+ */
34
+ export async function buildStemM4a(options) {
35
+ const { outputPath, stems, metadata, lyrics, pitch, llmCorrections, tags } = options;
36
+
37
+ // For now, use ffmpeg to mux stems into a single file
38
+ // The stem.m4a format requires custom atom injection
39
+ // We'll use the first stem as the main track and embed others as metadata
40
+
41
+ const ffmpegPath = getFFmpegPath();
42
+
43
+ // Build ffmpeg command to combine stems
44
+ // Using -map to include multiple audio streams
45
+ const args = [];
46
+
47
+ // NI Stems track order: master, drums, bass, other, vocals
48
+ const niStemOrder = ['master', 'drums', 'bass', 'other', 'vocals'];
49
+ const stemNames = niStemOrder.filter((name) => stems[name]);
50
+
51
+ // Add input files in correct order
52
+ for (const name of stemNames) {
53
+ args.push('-i', stems[name]);
54
+ }
55
+
56
+ // Map all inputs to output
57
+ for (let i = 0; i < stemNames.length; i++) {
58
+ args.push('-map', `${i}:a`);
59
+ }
60
+
61
+ // Set metadata - copy ALL original ID3 tags
62
+ const id3Tags = metadata.tags || {};
63
+
64
+ // Standard ID3 tags to preserve
65
+ const tagMapping = {
66
+ title: metadata.title || id3Tags.title,
67
+ artist: metadata.artist || id3Tags.artist,
68
+ album: id3Tags.album,
69
+ album_artist: id3Tags.album_artist || id3Tags.albumartist,
70
+ composer: id3Tags.composer,
71
+ genre: id3Tags.genre,
72
+ date: id3Tags.date || id3Tags.year,
73
+ track: id3Tags.track || id3Tags.tracknumber,
74
+ disc: id3Tags.disc || id3Tags.discnumber,
75
+ comment: id3Tags.comment,
76
+ copyright: id3Tags.copyright,
77
+ publisher: id3Tags.publisher,
78
+ encoded_by: id3Tags.encoded_by,
79
+ language: id3Tags.language,
80
+ lyrics: id3Tags.lyrics || id3Tags.unsyncedlyrics,
81
+ bpm: id3Tags.bpm || id3Tags.tbpm,
82
+ initialkey: pitch?.detected_key?.key || id3Tags.initialkey || id3Tags.key,
83
+ isrc: id3Tags.isrc,
84
+ barcode: id3Tags.barcode,
85
+ catalog: id3Tags.catalog,
86
+ compilation: id3Tags.compilation,
87
+ grouping: id3Tags.grouping,
88
+ };
89
+
90
+ // Add all non-empty tags
91
+ for (const [key, value] of Object.entries(tagMapping)) {
92
+ if (value) {
93
+ args.push('-metadata', `${key}=${value}`);
94
+ }
95
+ }
96
+
97
+ // Also pass through any additional ID3 tags we might have missed
98
+ for (const [key, value] of Object.entries(id3Tags)) {
99
+ const lowerKey = key.toLowerCase();
100
+ // Skip if already handled above
101
+ if (!tagMapping[lowerKey] && value) {
102
+ args.push('-metadata', `${key}=${value}`);
103
+ }
104
+ }
105
+
106
+ args.push('-metadata', 'encoder=Loukai Creator');
107
+
108
+ // Log key if detected
109
+ if (pitch?.detected_key?.key) {
110
+ console.log(`šŸŽµ Writing key to metadata: ${pitch.detected_key.key}`);
111
+ }
112
+
113
+ // Copy codecs (stems are already AAC)
114
+ args.push('-c', 'copy');
115
+
116
+ // Add stream labels for stems
117
+ for (let i = 0; i < stemNames.length; i++) {
118
+ const stemName = stemNames[i];
119
+ // Use metadata to label streams
120
+ args.push(`-metadata:s:a:${i}`, `title=${stemName}`);
121
+ }
122
+
123
+ // Per NI Stems spec: Track 1 (master) should be "enabled"/default,
124
+ // Tracks 2-5 (stems) should be "disabled" so normal players only play master
125
+ args.push('-disposition:a:0', 'default'); // Master track is default
126
+ for (let i = 1; i < stemNames.length; i++) {
127
+ args.push(`-disposition:a:${i}`, '0'); // Clear disposition flags for stem tracks
128
+ }
129
+
130
+ // Output format
131
+ args.push('-f', 'mp4');
132
+ args.push('-y'); // Overwrite output
133
+ args.push(outputPath);
134
+
135
+ // Run ffmpeg
136
+ await new Promise((resolve, reject) => {
137
+ const proc = spawn(ffmpegPath, args, {
138
+ stdio: ['pipe', 'pipe', 'pipe'],
139
+ });
140
+
141
+ let stderr = '';
142
+ proc.stderr.on('data', (data) => {
143
+ stderr += data.toString();
144
+ });
145
+
146
+ proc.on('close', (code) => {
147
+ if (code === 0) {
148
+ resolve();
149
+ } else {
150
+ reject(new Error(`FFmpeg failed (code ${code}): ${stderr.slice(-500)}`));
151
+ }
152
+ });
153
+
154
+ proc.on('error', (err) => {
155
+ reject(new Error(`Failed to run FFmpeg: ${err.message}`));
156
+ });
157
+ });
158
+
159
+ // Add NI Stems metadata so Mixxx/Traktor recognize this as a stem file
160
+ // Per NI Stems spec, stems array should have exactly 4 entries (NOT including master)
161
+ // Track order: drums, bass, other, vocals (corresponding to tracks 2-5)
162
+ const stemPartsOnly = stemNames.filter((name) => name !== 'master');
163
+ console.log(
164
+ `šŸŽ›ļø Writing NI Stems metadata for ${stemPartsOnly.length} stem parts: ${stemPartsOnly.join(', ')}`
165
+ );
166
+ await M4AAtoms.addNiStemsMetadata(outputPath, stemPartsOnly);
167
+
168
+ // Verify stem atom was written (debug)
169
+ const { stat } = await import('fs/promises');
170
+ const afterStemSize = (await stat(outputPath)).size;
171
+ console.log(`šŸ“Š File size after stem atom: ${afterStemSize} bytes`);
172
+
173
+ // Now inject kara atom for karaoke data using m4a-stems library
174
+ await injectKaraokeAtoms(outputPath, {
175
+ lyrics,
176
+ pitch,
177
+ metadata,
178
+ stems: stemNames,
179
+ llmCorrections,
180
+ tags,
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Inject karaoke atoms into an M4A file using m4a-stems library
186
+ *
187
+ * @param {string} filePath - Path to M4A file
188
+ * @param {Object} data - Karaoke data to embed
189
+ */
190
+ async function injectKaraokeAtoms(filePath, data) {
191
+ const { lyrics, llmCorrections, tags } = data;
192
+
193
+ // Convert lyrics segments to lines format expected by kara atom
194
+ // Include word-level timing if available from Whisper
195
+ const lines = [];
196
+ if (lyrics && lyrics.lines && lyrics.lines.length > 0) {
197
+ const words = lyrics.words || [];
198
+
199
+ for (const line of lyrics.lines) {
200
+ const lineData = {
201
+ start: line.start,
202
+ end: line.end,
203
+ text: line.text,
204
+ };
205
+
206
+ // Find words that fall within this line's time range
207
+ const lineWords = words.filter((w) => w.start >= line.start && w.start < line.end);
208
+
209
+ if (lineWords.length > 0) {
210
+ // Compute relative timings: [startOffset, endOffset] from line.start
211
+ // Round to 3 decimal places for reasonable precision
212
+ const timings = lineWords.map((w) => [
213
+ Math.round((w.start - line.start) * 1000) / 1000,
214
+ Math.round(((w.end || w.start + 0.1) - line.start) * 1000) / 1000,
215
+ ]);
216
+ lineData.words = { timings };
217
+ }
218
+
219
+ lines.push(lineData);
220
+ }
221
+ }
222
+
223
+ // Build kara data structure for m4a-stems
224
+ // Note: Audio sources are read from the NI Stems 'stem' atom, not stored in kara
225
+ const karaData = {
226
+ // Timing information
227
+ timing: {
228
+ offset_sec: 0,
229
+ encoder_delay_samples: 0,
230
+ },
231
+
232
+ // Tags for filtering (e.g., 'edited', 'ai_corrected')
233
+ tags: tags || [],
234
+
235
+ // Lyrics (lines)
236
+ lines: lines,
237
+ };
238
+
239
+ // Add LLM corrections metadata if available
240
+ // Uses same structure as KAI format for consistency with SongEditor
241
+ if (
242
+ llmCorrections &&
243
+ (llmCorrections.corrections?.length > 0 || llmCorrections.missing_lines?.length > 0)
244
+ ) {
245
+ karaData.meta = {
246
+ corrections: {
247
+ // Applied corrections (for reference/audit)
248
+ applied: (llmCorrections.corrections || []).map((c) => ({
249
+ line: c.line_num,
250
+ start: c.start_time,
251
+ end: c.end_time,
252
+ old: c.old_text,
253
+ new: c.new_text,
254
+ reason: c.reason,
255
+ word_retention: c.retention_rate,
256
+ })),
257
+ // Suggested missing lines (user can review/add in editor)
258
+ missing_lines_suggested: (llmCorrections.missing_lines || []).map((s) => ({
259
+ suggested_text: s.suggested_text,
260
+ start: s.start_time,
261
+ end: s.end_time,
262
+ confidence: s.confidence,
263
+ reason: s.reason,
264
+ })),
265
+ // Stats
266
+ provider: llmCorrections.provider,
267
+ model: llmCorrections.model,
268
+ },
269
+ };
270
+ }
271
+
272
+ // Write kara atom using m4a-stems library
273
+ console.log(`šŸ’¾ Writing kara atom: ${lines.length} lines`);
274
+ await M4AAtoms.writeKaraAtom(filePath, karaData);
275
+
276
+ // Verify final file size (debug)
277
+ const { stat } = await import('fs/promises');
278
+ const finalSize = (await stat(filePath)).size;
279
+ console.log(`šŸ“Š Final file size after kara atom: ${finalSize} bytes`);
280
+
281
+ // Note: Vocal pitch tracking is done at runtime, not stored in file.
282
+ // CREPE output is used only for key detection (stored in standard metadata).
283
+
284
+ console.log('āœ… Karaoke atoms written successfully');
285
+ }
286
+
287
+ /**
288
+ * Inject lyrics into an existing .stem.m4a file
289
+ * Used for "lyrics only" mode when stems already exist
290
+ *
291
+ * @param {Object} options - Injection options
292
+ * @param {string} options.filePath - Path to existing .stem.m4a file
293
+ * @param {Object} options.lyrics - Whisper transcription result with word timestamps
294
+ * @param {Object} options.llmCorrections - LLM correction stats
295
+ * @param {string[]} options.tags - Tags array for filtering
296
+ * @returns {Promise<void>}
297
+ */
298
+ export async function injectLyricsIntoStemFile(options) {
299
+ const { filePath, lyrics, llmCorrections, tags } = options;
300
+
301
+ console.log(`šŸŽ¤ Injecting lyrics into existing stem file: ${filePath}`);
302
+
303
+ // Read existing kara atom to preserve timing/tags
304
+ let existingKara = null;
305
+ try {
306
+ existingKara = await M4AAtoms.readKaraAtom(filePath);
307
+ } catch {
308
+ // No existing kara atom - that's fine
309
+ }
310
+
311
+ // Build kara data structure with word-level timing if available
312
+ const lines = [];
313
+ if (lyrics && lyrics.lines && lyrics.lines.length > 0) {
314
+ const words = lyrics.words || [];
315
+
316
+ for (const line of lyrics.lines) {
317
+ const lineData = {
318
+ start: line.start,
319
+ end: line.end,
320
+ text: line.text,
321
+ };
322
+
323
+ // Find words that fall within this line's time range
324
+ const lineWords = words.filter((w) => w.start >= line.start && w.start < line.end);
325
+
326
+ if (lineWords.length > 0) {
327
+ // Compute relative timings: [startOffset, endOffset] from line.start
328
+ const timings = lineWords.map((w) => [
329
+ Math.round((w.start - line.start) * 1000) / 1000,
330
+ Math.round(((w.end || w.start + 0.1) - line.start) * 1000) / 1000,
331
+ ]);
332
+ lineData.words = { timings };
333
+ }
334
+
335
+ lines.push(lineData);
336
+ }
337
+ }
338
+
339
+ // Note: Audio sources are read from the NI Stems 'stem' atom, not stored in kara
340
+ const karaData = {
341
+ timing: {
342
+ offset_sec: existingKara?.timing?.offset_sec || 0,
343
+ encoder_delay_samples: existingKara?.timing?.encoder_delay_samples || 0,
344
+ },
345
+ tags: tags || [],
346
+ lines: lines,
347
+ };
348
+
349
+ // Add LLM corrections metadata if available
350
+ if (
351
+ llmCorrections &&
352
+ (llmCorrections.corrections?.length > 0 || llmCorrections.missing_lines?.length > 0)
353
+ ) {
354
+ karaData.meta = {
355
+ corrections: {
356
+ applied: (llmCorrections.corrections || []).map((c) => ({
357
+ line: c.line_num,
358
+ start: c.start_time,
359
+ end: c.end_time,
360
+ old: c.old_text,
361
+ new: c.new_text,
362
+ reason: c.reason,
363
+ word_retention: c.retention_rate,
364
+ })),
365
+ missing_lines_suggested: (llmCorrections.missing_lines || []).map((s) => ({
366
+ suggested_text: s.suggested_text,
367
+ start: s.start_time,
368
+ end: s.end_time,
369
+ confidence: s.confidence,
370
+ reason: s.reason,
371
+ })),
372
+ provider: llmCorrections.provider,
373
+ model: llmCorrections.model,
374
+ },
375
+ };
376
+ }
377
+
378
+ // Write kara atom
379
+ console.log(`šŸ’¾ Writing kara atom: ${lines.length} lines`);
380
+ await M4AAtoms.writeKaraAtom(filePath, karaData);
381
+
382
+ console.log('āœ… Lyrics injected successfully');
383
+ }
384
+
385
+ /**
386
+ * Repair an existing .stem.m4a file to fix NI Stems metadata
387
+ * This fixes files created before the spec-compliant stem atom was implemented
388
+ *
389
+ * @param {string} filePath - Path to existing .stem.m4a file
390
+ * @param {Object} options - Repair options
391
+ * @param {boolean} options.force - Force rewrite even if metadata exists
392
+ * @returns {Promise<Object>} Repair result
393
+ */
394
+ export async function repairStemFile(filePath, options = {}) {
395
+ console.log(`šŸ”§ Checking stem file: ${filePath}`);
396
+
397
+ // Default NI Stems order (excluding master, which is track 0)
398
+ const stemPartsOnly = ['drums', 'bass', 'other', 'vocals'];
399
+
400
+ try {
401
+ // Check if NI Stems metadata already exists
402
+ let existingMetadata = null;
403
+ try {
404
+ existingMetadata = await M4AAtoms.readNiStemsMetadata(filePath);
405
+ } catch {
406
+ // No existing metadata
407
+ }
408
+
409
+ if (existingMetadata && existingMetadata.stems && !options.force) {
410
+ const existingStems = existingMetadata.stems.map((s) => s.name).join(', ');
411
+ console.log(`āœ… File already has valid NI Stems metadata: ${existingStems}`);
412
+ console.log(' Use --force to rewrite anyway.');
413
+ return {
414
+ success: true,
415
+ filePath,
416
+ alreadyValid: true,
417
+ existingStems: existingMetadata.stems.map((s) => s.name),
418
+ };
419
+ }
420
+
421
+ // Write the stem atom with correct 4-stem metadata
422
+ if (existingMetadata) {
423
+ console.log(`šŸ”„ Force rewriting NI Stems metadata for ${stemPartsOnly.length} stem parts`);
424
+ } else {
425
+ console.log(`šŸŽ›ļø Adding NI Stems metadata for ${stemPartsOnly.length} stem parts`);
426
+ }
427
+ await M4AAtoms.addNiStemsMetadata(filePath, stemPartsOnly);
428
+
429
+ console.log('āœ… Stem file repaired successfully');
430
+ console.log('āš ļø Note: Track disposition flags cannot be fixed without re-encoding.');
431
+ console.log(' File should work in Mixxx/Traktor but may play wrong track in some players.');
432
+
433
+ return {
434
+ success: true,
435
+ filePath,
436
+ stemsFixed: stemPartsOnly,
437
+ };
438
+ } catch (error) {
439
+ console.error('āŒ Failed to repair stem file:', error.message);
440
+ return {
441
+ success: false,
442
+ error: error.message,
443
+ };
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Batch repair multiple stem files
449
+ * @param {string[]} filePaths - Array of paths to .stem.m4a files
450
+ * @param {Object} options - Repair options (passed to each repairStemFile call)
451
+ * @returns {Promise<Object>} Batch repair results
452
+ */
453
+ export async function repairStemFiles(filePaths, options = {}) {
454
+ console.log(`šŸ”§ Batch checking ${filePaths.length} stem files...`);
455
+
456
+ const results = {
457
+ total: filePaths.length,
458
+ success: 0,
459
+ failed: 0,
460
+ alreadyValid: 0,
461
+ repaired: 0,
462
+ files: [],
463
+ };
464
+
465
+ for (const filePath of filePaths) {
466
+ const result = await repairStemFile(filePath, options);
467
+ results.files.push(result);
468
+ if (result.success) {
469
+ results.success++;
470
+ if (result.alreadyValid) {
471
+ results.alreadyValid++;
472
+ } else {
473
+ results.repaired++;
474
+ }
475
+ } else {
476
+ results.failed++;
477
+ }
478
+ }
479
+
480
+ console.log(
481
+ `\nšŸ“Š Complete: ${results.alreadyValid} already valid, ${results.repaired} repaired, ${results.failed} failed`
482
+ );
483
+ return results;
484
+ }
485
+
486
+ export default {
487
+ buildStemM4a,
488
+ injectLyricsIntoStemFile,
489
+ repairStemFile,
490
+ repairStemFiles,
491
+ };