loukai-app 0.4.0 → 0.4.2

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 (29) hide show
  1. package/README.md +5 -5
  2. package/package.json +2 -2
  3. package/src/main/creator/conversionService.js +5 -5
  4. package/src/main/creator/pythonRunner.js +3 -2
  5. package/src/main/creator/stemBuilder.js +9 -9
  6. package/src/main/handlers/fileHandlers.js +1 -1
  7. package/src/main/handlers/libraryHandlers.js +3 -0
  8. package/src/main/webServer.js +2 -2
  9. package/src/renderer/components/creator/CreateTab.jsx +6 -6
  10. package/src/renderer/dist/assets/{kaiPlayer-CoMx__a_.js → kaiPlayer-DSaY7TxC.js} +2 -2
  11. package/src/renderer/dist/assets/kaiPlayer-DSaY7TxC.js.map +1 -0
  12. package/src/renderer/dist/assets/songLoaders-CcYVonLu.js +2 -0
  13. package/src/renderer/dist/assets/{songLoaders-BaTgGib4.js.map → songLoaders-CcYVonLu.js.map} +1 -1
  14. package/src/renderer/dist/renderer.css +1 -1
  15. package/src/renderer/dist/renderer.js +11 -51
  16. package/src/renderer/dist/renderer.js.map +1 -1
  17. package/src/renderer/js/kaiPlayer.js +9 -7
  18. package/src/renderer/lib/cdgraphics.js +0 -1
  19. package/src/shared/services/creatorService.js +2 -2
  20. package/src/shared/services/libraryService.js +4 -1
  21. package/src/utils/m4aLoader.js +1 -1
  22. package/src/web/dist/assets/index-CGbmW1VG.js +11 -0
  23. package/src/web/dist/assets/{index-0H-RnRrV.js.map → index-CGbmW1VG.js.map} +1 -1
  24. package/src/web/dist/assets/index-GLKJK41r.css +1 -0
  25. package/src/web/dist/index.html +2 -2
  26. package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js.map +0 -1
  27. package/src/renderer/dist/assets/songLoaders-BaTgGib4.js +0 -2
  28. package/src/web/dist/assets/index-0H-RnRrV.js +0 -51
  29. package/src/web/dist/assets/index-DYW2zB0u.css +0 -1
package/README.md CHANGED
@@ -267,7 +267,7 @@ Loukai is built with a multi-process architecture:
267
267
  - **Multi-track audio**: Master + 4 stems (drums, bass, other, vocals)
268
268
  - **NI Stems atom**: `stem` - standard metadata for DJ software compatibility
269
269
  - **Karaoke atom**: `kara` (lyrics with word-level timing)
270
- - **File extension**: `.stem.m4a` or `.m4a`
270
+ - **File extension**: `.stem.mp4` or `.m4a`
271
271
 
272
272
  **Full specification:** [docs/m4a_format.md](./docs/m4a_format.md)
273
273
 
@@ -280,9 +280,9 @@ Use the built-in **Creator** tab in Loukai:
280
280
  - Separate audio into stems using Demucs (AI stem separation)
281
281
  - Transcribe lyrics using Whisper (AI speech recognition)
282
282
  - Detect musical key using CREPE
283
- - Package everything into a `.stem.m4a` file
283
+ - Package everything into a `.stem.mp4` file
284
284
 
285
- **Output:** `Artist - Title.stem.m4a` saved to your songs folder
285
+ **Output:** `Artist - Title.stem.mp4` saved to your songs folder
286
286
 
287
287
  ### CDG Format (Legacy)
288
288
 
@@ -327,7 +327,7 @@ All settings are automatically saved to:
327
327
  2. **Scan Library**: Click "Scan Library" to index all songs
328
328
  3. **Search & Play**: Use the Library tab to find and play songs
329
329
 
330
- **Tip:** For best results, use `.stem.m4a` files created with the built-in Creator. M4A files load faster and take less disk space than legacy formats.
330
+ **Tip:** For best results, use `.stem.mp4` files created with the built-in Creator. M4A files load faster and take less disk space than legacy formats.
331
331
 
332
332
  ### Playing Karaoke
333
333
 
@@ -503,7 +503,7 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for development guidelines.
503
503
  ### Library Not Scanning
504
504
  - Ensure songs folder path is correct
505
505
  - Check file permissions (read access required)
506
- - Supported formats: `.m4a` (recommended), `.stem.m4a`, `.cdg` + `.mp3` pairs
506
+ - Supported formats: `.m4a` (recommended), `.stem.mp4`, `.cdg` + `.mp3` pairs
507
507
  - For best performance, use M4A Stems format
508
508
 
509
509
  ### Web Server Not Accessible
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loukai-app",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "Loukai Karaoke - Free and open source karaoke system for playing and creating stem-based karaoke files",
6
6
  "main": "src/main/main.js",
@@ -38,6 +38,7 @@
38
38
  "author": "Luis Montes",
39
39
  "license": "AGPL-3.0",
40
40
  "devDependencies": {
41
+ "@electron/rebuild": "^4.0.4",
41
42
  "@eslint/js": "^9.37.0",
42
43
  "@testing-library/jest-dom": "^6.9.1",
43
44
  "@testing-library/react": "^16.3.0",
@@ -47,7 +48,6 @@
47
48
  "@vitest/ui": "^3.2.4",
48
49
  "autoprefixer": "^10.4.21",
49
50
  "electron-builder": "^26.0.12",
50
- "electron-rebuild": "^3.2.9",
51
51
  "eslint": "^9.37.0",
52
52
  "eslint-config-prettier": "^10.1.8",
53
53
  "eslint-plugin-import": "^2.32.0",
@@ -7,7 +7,7 @@ import { log } from '../logger.js';
7
7
  * 2. Run Demucs stem separation
8
8
  * 3. Run Whisper transcription on vocals
9
9
  * 4. Run CREPE pitch detection on vocals (optional)
10
- * 5. Assemble into .stem.m4a file
10
+ * 5. Assemble into .stem.mp4 file
11
11
  */
12
12
 
13
13
  import { join, dirname } from 'path';
@@ -369,7 +369,7 @@ export async function runConversion(
369
369
  checkCancelled();
370
370
 
371
371
  // Output to same directory with modified name, or optionally overwrite in place
372
- outputPath = join(outputDir, `${safeFileName}.stem.m4a`);
372
+ outputPath = join(outputDir, `${safeFileName}.stem.mp4`);
373
373
 
374
374
  // Copy original file to output location if different
375
375
  if (inputPath !== outputPath) {
@@ -436,11 +436,11 @@ export async function runConversion(
436
436
  checkCancelled();
437
437
  }
438
438
 
439
- // Build .stem.m4a (95-100%)
440
- onProgress('build', `[${STEPS.build}] Packaging stem.m4a file...`, 95);
439
+ // Build .stem.mp4 (95-100%)
440
+ onProgress('build', `[${STEPS.build}] Packaging stem.mp4 file...`, 95);
441
441
  checkCancelled();
442
442
 
443
- outputPath = join(outputDir, `${safeFileName}.stem.m4a`);
443
+ outputPath = join(outputDir, `${safeFileName}.stem.mp4`);
444
444
 
445
445
  await buildStemM4a({
446
446
  outputPath,
@@ -76,9 +76,10 @@ export function runPythonScript(
76
76
  const lines = text.split('\n');
77
77
 
78
78
  for (const line of lines) {
79
- // Emit raw console output
79
+ // Emit raw console output (round long floats so tqdm progress doesn't jitter)
80
80
  if (onConsoleOutput && line.trim()) {
81
- onConsoleOutput(line);
81
+ const cleaned = line.replace(/\d+\.\d{3,}/g, (m) => parseFloat(m).toFixed(2));
82
+ onConsoleOutput(cleaned);
82
83
  }
83
84
 
84
85
  // Parse our PROGRESS: updates
@@ -1,8 +1,8 @@
1
1
  import { log } from '../logger.js';
2
2
  /**
3
- * Stem Builder - Creates .stem.m4a files with embedded stem data
3
+ * Stem Builder - Creates .stem.mp4 files with embedded stem data
4
4
  *
5
- * The .stem.m4a format embeds multiple audio stems in a single M4A container
5
+ * The .stem.mp4 format embeds multiple audio stems in a single M4A container
6
6
  * using custom atoms/boxes. This is compatible with Native Instruments Stems.
7
7
  *
8
8
  * Structure:
@@ -21,10 +21,10 @@ import { getFFmpegPath } from './systemChecker.js';
21
21
  import { Atoms as M4AAtoms } from 'm4a-stems';
22
22
 
23
23
  /**
24
- * Build a .stem.m4a file from individual stem files
24
+ * Build a .stem.mp4 file from individual stem files
25
25
  *
26
26
  * @param {Object} options - Build options
27
- * @param {string} options.outputPath - Output .stem.m4a path
27
+ * @param {string} options.outputPath - Output .stem.mp4 path
28
28
  * @param {Object} options.stems - Map of stem name to path
29
29
  * @param {Object} options.metadata - Song metadata (title, artist, duration)
30
30
  * @param {Object} options.lyrics - Whisper transcription result with word timestamps
@@ -286,11 +286,11 @@ async function injectKaraokeAtoms(filePath, data) {
286
286
  }
287
287
 
288
288
  /**
289
- * Inject lyrics into an existing .stem.m4a file
289
+ * Inject lyrics into an existing .stem.mp4 file
290
290
  * Used for "lyrics only" mode when stems already exist
291
291
  *
292
292
  * @param {Object} options - Injection options
293
- * @param {string} options.filePath - Path to existing .stem.m4a file
293
+ * @param {string} options.filePath - Path to existing .stem.mp4 file
294
294
  * @param {Object} options.lyrics - Whisper transcription result with word timestamps
295
295
  * @param {Object} options.llmCorrections - LLM correction stats
296
296
  * @param {string[]} options.tags - Tags array for filtering
@@ -384,10 +384,10 @@ export async function injectLyricsIntoStemFile(options) {
384
384
  }
385
385
 
386
386
  /**
387
- * Repair an existing .stem.m4a file to fix NI Stems metadata
387
+ * Repair an existing .stem.mp4 file to fix NI Stems metadata
388
388
  * This fixes files created before the spec-compliant stem atom was implemented
389
389
  *
390
- * @param {string} filePath - Path to existing .stem.m4a file
390
+ * @param {string} filePath - Path to existing .stem.mp4 file
391
391
  * @param {Object} options - Repair options
392
392
  * @param {boolean} options.force - Force rewrite even if metadata exists
393
393
  * @returns {Promise<Object>} Repair result
@@ -447,7 +447,7 @@ export async function repairStemFile(filePath, options = {}) {
447
447
 
448
448
  /**
449
449
  * Batch repair multiple stem files
450
- * @param {string[]} filePaths - Array of paths to .stem.m4a files
450
+ * @param {string[]} filePaths - Array of paths to .stem.mp4 files
451
451
  * @param {Object} options - Repair options (passed to each repairStemFile call)
452
452
  * @returns {Promise<Object>} Batch repair results
453
453
  */
@@ -32,7 +32,7 @@ export function registerFileHandlers(mainApp) {
32
32
  ipcMain.handle('file:loadKaiFromPath', async (event, filePath) => {
33
33
  // Get the songs folder from settings
34
34
  const songsFolder = mainApp.settings?.getSongsFolder?.();
35
-
35
+
36
36
  // Validate the path is within the songs directory
37
37
  const validation = validateSongPath(filePath, songsFolder);
38
38
  if (!validation.valid) {
@@ -58,6 +58,9 @@ export function registerLibraryHandlers(mainApp) {
58
58
  // Update all caches (mainApp, webServer, disk)
59
59
  await libraryService.updateLibraryCache(mainApp, result.files);
60
60
 
61
+ // Notify renderer so LibraryPanel reloads (matches scanLibrary behavior)
62
+ mainApp.sendToRenderer('library:scanComplete', { count: result.files.length });
63
+
61
64
  // Return with 'songs' key for renderer compatibility
62
65
  return {
63
66
  ...result,
@@ -1928,11 +1928,11 @@ class WebServer {
1928
1928
  // Get audio files that can be converted (from library or direct path)
1929
1929
  this.app.get('/admin/creator/sources', async (req, res) => {
1930
1930
  try {
1931
- // Get library songs that are audio files (not already .stem.m4a)
1931
+ // Get library songs that are audio files (not already .stem.mp4)
1932
1932
  const allSongs = await this.getCachedSongs();
1933
1933
 
1934
1934
  // Filter to songs that could be source files for conversion
1935
- // (exclude .stem.m4a which are already karaoke files)
1935
+ // (exclude .stem.mp4 which are already karaoke files)
1936
1936
  const sourceCandidates = allSongs.filter((song) => {
1937
1937
  const ext = song.path.split('.').pop().toLowerCase();
1938
1938
  return [
@@ -6,7 +6,7 @@
6
6
  * 2. Select audio file
7
7
  * 3. Configure options (stems, whisper model, etc.)
8
8
  * 4. Run conversion pipeline
9
- * 5. Output .stem.m4a file
9
+ * 5. Output .stem.mp4 file
10
10
  */
11
11
 
12
12
  import { useState, useEffect, useCallback, useRef } from 'react';
@@ -133,7 +133,7 @@ export function CreateTab({ bridge: _bridge }) {
133
133
  const [options, setOptions] = useState({
134
134
  title: '',
135
135
  artist: '',
136
- numStems: 4, // Always 4 stems for .stem.m4a format
136
+ numStems: 4, // Always 4 stems for .stem.mp4 format
137
137
  language: 'en',
138
138
  referenceLyrics: '',
139
139
  });
@@ -391,9 +391,9 @@ export function CreateTab({ bridge: _bridge }) {
391
391
  // Get output directory based on settings
392
392
  let outputDir = undefined; // Default: same directory as source file
393
393
  if (outputToSongsFolder) {
394
- const songsFolder = await window.kaiAPI?.library?.getSongsFolder?.();
395
- if (songsFolder) {
396
- outputDir = songsFolder;
394
+ const result = await window.kaiAPI?.library?.getSongsFolder?.();
395
+ if (result?.folder) {
396
+ outputDir = result.folder;
397
397
  }
398
398
  }
399
399
 
@@ -834,7 +834,7 @@ export function CreateTab({ bridge: _bridge }) {
834
834
  </span>
835
835
  </label>
836
836
  <p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
837
- When enabled, created .stem.m4a files will be saved to your configured songs library
837
+ When enabled, created .stem.mp4 files will be saved to your configured songs library
838
838
  folder instead of next to the source file.
839
839
  </p>
840
840
  </div>
@@ -1,2 +1,2 @@
1
- import{P as c,M as d}from"./microphoneEngine-BaCUhhQc.js";class l extends c{constructor(){super(),this.audioContexts={PA:null,IEM:null},this.outputDevices={PA:"default",IEM:"default"},this.currentPosition=0,this.songData=null,this.audioBuffers=new Map,this.outputNodes={PA:{sourceNodes:new Map,gainNodes:new Map,masterGain:null,analyser:null,vocalsPAGain:null},IEM:{sourceNodes:new Map,gainNodes:new Map,masterGain:null}},this.vocalsPAEnabled=!1,this.startTime=0,this.pauseTime=0,this.micEngine=null,this.mixerState={PA:{gain:0,muted:!1},IEM:{gain:0,muted:!0,mono:!0},mic:{gain:0,muted:!0},stems:[],micToSpeakers:!0,enableMic:!0}}async initialize(){try{const t=await this.getAvailableOutputDevices();await this.loadDevicePreferences(t);const i={};this.outputDevices.PA!=="default"&&"sinkId"in AudioContext.prototype&&(i.sinkId=this.outputDevices.PA),this.audioContexts.PA=new(window.AudioContext||window.webkitAudioContext)(i),this.outputNodes.PA.masterGain=this.audioContexts.PA.createGain(),this.outputNodes.PA.masterGain.connect(this.audioContexts.PA.destination);const e=this.mixerState.PA.muted?0:this.dbToLinear(this.mixerState.PA.gain);this.outputNodes.PA.masterGain.gain.value=e,this.outputNodes.PA.analyser=this.audioContexts.PA.createAnalyser(),this.outputNodes.PA.analyser.fftSize=2048,this.outputNodes.PA.analyser.smoothingTimeConstant=.8,this.outputNodes.PA.vocalsPAGain=this.audioContexts.PA.createGain(),this.outputNodes.PA.vocalsPAGain.gain.value=0,this.outputNodes.PA.vocalsPAGain.connect(this.outputNodes.PA.masterGain);const s={};this.outputDevices.IEM!=="default"&&"sinkId"in AudioContext.prototype&&(s.sinkId=this.outputDevices.IEM),this.audioContexts.IEM=new(window.AudioContext||window.webkitAudioContext)(s),this.outputNodes.IEM.masterGain=this.audioContexts.IEM.createGain(),this.outputNodes.IEM.masterGain.connect(this.audioContexts.IEM.destination);const n=this.mixerState.IEM.muted?0:this.dbToLinear(this.mixerState.IEM.gain);if(this.outputNodes.IEM.masterGain.gain.value=n,this.micEngine=new d(this.audioContexts.PA,this.outputNodes.PA.masterGain,{getCurrentPosition:()=>this.getCurrentPosition()}),await this.micEngine.loadAutoTuneWorklet(),await this.loadMicSettings(),this.micEngine.enableMic){await this.micEngine.startMicrophoneInput(this.micEngine.inputDevice);const a=this.mixerState.mic.muted?0:this.dbToLinear(this.mixerState.mic.gain);this.micEngine.setMicrophoneGain(a)}return!0}catch(t){return console.warn("⚠️ Audio initialization issue:",t.message),!1}}resolveDeviceByLabel(t,i,e){if(!i||!i.id&&!i.name||i.deviceKind==="default"||i.id==="")return"default";if(i.name){const s=t.find(n=>n.label===i.name);if(s)return s.deviceId!==i.id&&console.log(`🎧 ${e}: Matched "${i.name}" by label (ID changed)`),s.deviceId}if(i.id){const s=t.find(n=>n.deviceId===i.id);if(s)return s.deviceId}return console.warn(`🎧 ${e}: Saved device "${i.name||i.id}" not found, using default`),"default"}async loadDevicePreferences(t=[]){try{if(window.kaiAPI.settings){const i=await window.kaiAPI.settings.get("devicePreferences",null);if(i?.PA&&(this.outputDevices.PA=this.resolveDeviceByLabel(t,i.PA,"PA")),i?.IEM&&(this.outputDevices.IEM=this.resolveDeviceByLabel(t,i.IEM,"IEM")),i?.input){const e=await this.getAvailableInputDevices();this.inputDevice=this.resolveDeviceByLabel(e,i.input,"input")}}if(window.kaiAPI?.app){const i=await window.kaiAPI.app.getState();i?.mixer&&(typeof i.mixer.PA?.gain=="number"&&(this.mixerState.PA.gain=i.mixer.PA.gain),typeof i.mixer.PA?.muted=="boolean"&&(this.mixerState.PA.muted=i.mixer.PA.muted),typeof i.mixer.IEM?.gain=="number"&&(this.mixerState.IEM.gain=i.mixer.IEM.gain),typeof i.mixer.IEM?.muted=="boolean"&&(this.mixerState.IEM.muted=i.mixer.IEM.muted),typeof i.mixer.mic?.gain=="number"&&(this.mixerState.mic.gain=i.mixer.mic.gain),typeof i.mixer.mic?.muted=="boolean"&&(this.mixerState.mic.muted=i.mixer.mic.muted))}}catch(i){console.warn("Failed to load device preferences:",i.message)}}async getAvailableOutputDevices(){try{return(await navigator.mediaDevices.enumerateDevices()).filter(i=>i.kind==="audiooutput")}catch(t){return console.warn("Failed to enumerate output devices:",t.message),[]}}async getAvailableInputDevices(){try{return(await navigator.mediaDevices.enumerateDevices()).filter(i=>i.kind==="audioinput")}catch(t){return console.warn("Failed to enumerate input devices:",t.message),[]}}async setOutputDevice(t,i){try{if(!["PA","IEM"].includes(t))return console.error("Invalid bus type:",t),!1;const e=this.isPlaying;e&&this.pause();const s=this.currentPosition;this.outputDevices[t]=i,this.audioContexts[t]&&await this.audioContexts[t].close();const n={};i!=="default"&&"sinkId"in AudioContext.prototype&&(n.sinkId=i),this.audioContexts[t]=new(window.AudioContext||window.webkitAudioContext)(n),this.outputNodes[t].masterGain=this.audioContexts[t].createGain(),this.outputNodes[t].masterGain.connect(this.audioContexts[t].destination);const a=this.mixerState[t].muted?0:this.dbToLinear(this.mixerState[t].gain);if(this.outputNodes[t].masterGain.gain.value=a,t==="PA"&&(this.outputNodes.PA.analyser=this.audioContexts.PA.createAnalyser(),this.outputNodes.PA.analyser.fftSize=2048,this.outputNodes.PA.analyser.smoothingTimeConstant=.8,this.outputNodes.PA.vocalsPAGain=this.audioContexts.PA.createGain(),this.outputNodes.PA.vocalsPAGain.gain.value=this.vocalsPAEnabled?this.dbToLinear(this.mixerState.IEM.gain):0,this.outputNodes.PA.vocalsPAGain.connect(this.outputNodes.PA.masterGain)),this.outputNodes[t].sourceNodes.clear(),this.outputNodes[t].gainNodes.clear(),t==="PA"&&this.micEngine&&(this.micEngine.updateAudioContext(this.audioContexts.PA,this.outputNodes.PA.masterGain),await this.micEngine.loadAutoTuneWorklet(),this.micEngine.enableMic)){await this.micEngine.startMicrophoneInput(this.micEngine.inputDevice);const u=this.mixerState.mic.muted?0:this.dbToLinear(this.mixerState.mic.gain);this.micEngine.setMicrophoneGain(u)}return this.songData&&await this.reloadAudioBuffersForBus(t),e&&this.songData&&(this.currentPosition=s,await this.play()),!0}catch(e){return console.error(`Failed to set ${t} output device:`,e),!1}}async loadSong(t){return this.songData=t,this.resetPosition(),this.currentPosition=0,this.startTime=0,this.pauseTime=0,this.monitoringStartTime=null,this.stopSongEndMonitoring(),this.mixerState.stems=(t.audio?.sources||[]).map((i,e)=>({id:i.name||i.filename,name:i.name||i.filename,gain:i.gain||0,index:e})),await this.loadAudioBuffers(t),this.micEngine&&this.micEngine.clearPitchReference(),this.reportSongLoaded(),this.reportStateChange(),!0}reportSongLoaded(){if(window.kaiAPI?.renderer&&this.songData){const t=this.getDuration();window.kaiAPI.renderer.songLoaded({path:this.songData.originalFilePath||this.songData.filePath,title:this.songData.metadata?.title||"Unknown",artist:this.songData.metadata?.artist||"Unknown",duration:t,isLoading:!1,format:"kai"}),this.reportMixerState()}}reportMixerState(){window.kaiAPI?.renderer&&this.mixerState&&window.kaiAPI.renderer.updateMixerState(this.mixerState)}async loadAudioBuffers(t){if(!t.audio?.sources){console.warn("No audio sources found in song data");return}(!this.audioContexts.PA||!this.audioContexts.IEM)&&await this.initialize();for(const e of t.audio.sources)try{if(e.audioData&&e.audioData.length>0){const s=e.audioData.buffer.slice(e.audioData.byteOffset,e.audioData.byteOffset+e.audioData.byteLength),n=await this.audioContexts.PA.decodeAudioData(s);this.audioBuffers.set(e.name,n)}else console.warn(`No audio data for source: ${e.name}`)}catch(s){console.error(`Failed to decode audio for ${e.name}:`,s)}let i=0;for(const[e,s]of this.audioBuffers)s.duration>i&&(i=s.duration);i>0?(this.songData.metadata||(this.songData.metadata={}),this.songData.metadata.duration=i):console.warn("No audio buffers loaded, duration remains 0")}async reloadAudioBuffersForBus(t){if(!(!this.songData?.audio?.sources||!this.audioContexts[t])){for(const i of this.songData.audio.sources)if(i.audioData&&i.audioData.length>0)try{if(!this.audioBuffers.has(i.name)){const e=i.audioData.buffer.slice(i.audioData.byteOffset,i.audioData.byteOffset+i.audioData.byteLength),s=await this.audioContexts[t].decodeAudioData(e);this.audioBuffers.set(i.name,s)}}catch(e){console.error(`Failed to reload audio buffer for ${t} - ${i.name}:`,e)}}}async play(){return this.songData?!this.audioContexts.PA||!this.audioContexts.IEM?(console.error("Audio contexts not initialized"),!1):(this.audioContexts.PA.state==="suspended"&&await this.audioContexts.PA.resume(),this.audioContexts.IEM.state==="suspended"&&await this.audioContexts.IEM.resume(),this.isPlaying=!0,this.stopAllSources(),this.createAudioGraph(),this.startAudioSources(),this.startSongEndMonitoring(),this.startStateReporting(),this.micEngine&&this.micEngine.setPlaying(!0),this.reportStateChange(),!0):(console.error("No song loaded"),!1)}pause(){return this.currentPosition=this.getCurrentPosition(),this.isPlaying=!1,this.audioContexts.PA&&(this.pauseTime=this.audioContexts.PA.currentTime),this.stopAllSources(),this.stopSongEndMonitoring(),this.stopStateReporting(),this.micEngine&&this.micEngine.setPlaying(!1),this.reportStateChange(),!0}seek(t){return this.currentPosition=t,this.isPlaying&&(this.stopAllSources(),this.startAudioSources()),this.reportStateChange(),!0}stopAllSources(){this.outputNodes.PA.sourceNodes.size+this.outputNodes.IEM.sourceNodes.size,this.outputNodes.PA.sourceNodes.forEach((t,i)=>{try{t.stop(),t.disconnect()}catch{}}),this.outputNodes.PA.sourceNodes.clear(),this.outputNodes.IEM.sourceNodes.forEach((t,i)=>{try{t.stop(),t.disconnect()}catch{}}),this.outputNodes.IEM.sourceNodes.clear()}createAudioGraph(){!this.audioContexts.PA||!this.audioContexts.IEM||(this.outputNodes.PA.gainNodes.clear(),this.outputNodes.IEM.gainNodes.clear(),this.mixerState.stems.forEach(t=>{const i=this.audioContexts.PA.createGain();i.connect(this.outputNodes.PA.masterGain),this.outputNodes.PA.gainNodes.set(t.name,i);const e=this.audioContexts.IEM.createGain();if(this.isVocalStem(t.name)&&this.mixerState.iemMonoVocals){const s=this.audioContexts.IEM.createChannelMerger(1);e.connect(s),s.connect(this.outputNodes.IEM.masterGain)}else e.connect(this.outputNodes.IEM.masterGain);this.outputNodes.IEM.gainNodes.set(t.name,e),this.updateStemGain(t)}))}startAudioSources(){if(!this.audioContexts.PA||!this.audioContexts.IEM)return;const t=Math.max(this.audioContexts.PA.currentTime,this.audioContexts.IEM.currentTime)+.1;this.startTime=t,this.mixerState.stems.forEach(i=>{if(this.isMixdownStem(i.name)){console.log(`⏭️ Skipping mixdown stem: ${i.name}`);return}const e=this.audioBuffers.get(i.name),s=this.outputNodes.PA.gainNodes.get(i.name),n=this.outputNodes.IEM.gainNodes.get(i.name);if(e&&s&&n)try{const a=Math.min(this.currentPosition,e.duration);if(this.isVocalStem(i.name)){const o=this.audioContexts.IEM.createBufferSource();if(o.buffer=e,o.connect(n),o.start(t,a),this.outputNodes.IEM.sourceNodes.set(i.name,o),this.outputNodes.PA.vocalsPAGain){const r=this.audioContexts.PA.createBufferSource();r.buffer=e,r.connect(this.outputNodes.PA.vocalsPAGain),this.micEngine&&this.micEngine.connectMusicSource(r),r.start(t,a),this.outputNodes.PA.sourceNodes.set(i.name+"_vocalsPA",r)}}else{const o=this.audioContexts.PA.createBufferSource();o.buffer=e,o.connect(s),this.outputNodes.PA.analyser&&o.connect(this.outputNodes.PA.analyser),this.isMelodicStem(i.name)&&this.micEngine&&this.micEngine.connectMusicSource(o),o.start(t,a),this.outputNodes.PA.sourceNodes.set(i.name,o),o.onended=()=>{this.isPlaying&&setTimeout(()=>this.checkForSongEnd(),10)}}}catch(a){console.error(`Failed to start source for ${i.name}:`,a)}else console.warn(`No audio buffer or gain nodes for stem: ${i.name}`)})}isVocalStem(t){const i=["vocals","vocal","voice","lead","singing","vox"],e=t.toLowerCase();return i.some(s=>e.includes(s))}isMixdownStem(t){const i=["mixdown","mix","master","full mix","stereo mix"],e=t.toLowerCase();return i.some(s=>e===s||e.includes(`_${s}`)||e.includes(`${s}_`))}isMelodicStem(t){const i=t.toLowerCase();return i.includes("other")||i.includes("music")||i.includes("instrumental")||i.includes("accompaniment")||i.includes("melody")?!0:!(this.isVocalStem(t)||i.includes("drum")||i.includes("percussion")||i.includes("bass"))}updateStemGain(t){const i=this.outputNodes.PA.gainNodes.get(t.name),e=this.outputNodes.IEM.gainNodes.get(t.name),s=this.isVocalStem(t.name);if(!this.audioContexts.PA||!this.audioContexts.IEM)return;const n=Math.pow(10,t.gain/20);s&&e?e.gain.setValueAtTime(n,this.audioContexts.IEM.currentTime):!s&&i&&i.gain.setValueAtTime(n,this.audioContexts.PA.currentTime)}setMasterGain(t,i){if(!["PA","IEM","mic"].includes(t))return!1;if(this.mixerState[t].gain=i,t==="PA"&&this.outputNodes.PA.masterGain){const e=this.dbToLinear(i);this.outputNodes.PA.masterGain.gain.setValueAtTime(e,this.audioContexts.PA.currentTime)}else if(t==="IEM"&&this.outputNodes.IEM.masterGain){const e=this.dbToLinear(i);this.outputNodes.IEM.masterGain.gain.setValueAtTime(e,this.audioContexts.IEM.currentTime)}else if(t==="mic"&&this.micEngine){const e=this.mixerState.mic.muted?0:this.dbToLinear(i);this.micEngine.setMicrophoneGain(e)}return this.reportMixerState(),!0}toggleMasterMute(t){if(!["PA","IEM","mic"].includes(t))return!1;this.mixerState[t].muted=!this.mixerState[t].muted;const i=this.mixerState[t].muted;if(t==="PA"&&this.outputNodes.PA.masterGain){const e=i?0:this.dbToLinear(this.mixerState.PA.gain);this.outputNodes.PA.masterGain.gain.setValueAtTime(e,this.audioContexts.PA.currentTime)}else if(t==="IEM"&&this.outputNodes.IEM.masterGain){const e=i?0:this.dbToLinear(this.mixerState.IEM.gain);this.outputNodes.IEM.masterGain.gain.setValueAtTime(e,this.audioContexts.IEM.currentTime)}else if(t==="mic"&&this.micEngine){const e=i?0:this.dbToLinear(this.mixerState.mic.gain);this.micEngine.setMicrophoneGain(e)}return this.reportMixerState(),!0}setMasterMute(t,i){if(!["PA","IEM","mic"].includes(t))return!1;if(this.mixerState[t].muted=i,t==="PA"&&this.outputNodes.PA.masterGain){const e=i?0:this.dbToLinear(this.mixerState.PA.gain);this.outputNodes.PA.masterGain.gain.setValueAtTime(e,this.audioContexts.PA.currentTime)}else if(t==="IEM"&&this.outputNodes.IEM.masterGain){const e=i?0:this.dbToLinear(this.mixerState.IEM.gain);this.outputNodes.IEM.masterGain.gain.setValueAtTime(e,this.audioContexts.IEM.currentTime)}else if(t==="mic"&&this.micEngine){const e=i?0:this.dbToLinear(this.mixerState.mic.gain);this.micEngine.setMicrophoneGain(e)}return!0}setVocalsPAEnabled(t,i=.05){if(!this.outputNodes.PA.vocalsPAGain||!this.audioContexts.PA||this.vocalsPAEnabled===t)return;this.vocalsPAEnabled=t;const e=t?this.dbToLinear(this.mixerState.IEM.gain):0,s=this.audioContexts.PA.currentTime;this.outputNodes.PA.vocalsPAGain.gain.cancelScheduledValues(s),this.outputNodes.PA.vocalsPAGain.gain.linearRampToValueAtTime(e,s+i)}getCurrentPosition(){if(this.isPlaying&&this.audioContexts.PA&&this.startTime>0){const t=this.audioContexts.PA.currentTime-this.startTime,i=this.currentPosition+t,e=this.getDuration();return e>0?Math.min(i,e):i}return this.currentPosition}getCurrentTime(){return this.getCurrentPosition()}getDuration(){return this.songData?.metadata?.duration||0}getMixerState(){return{PA:this.mixerState.PA,IEM:this.mixerState.IEM,mic:this.mixerState.mic,stems:this.mixerState.stems,isPlaying:this.isPlaying,position:this.getCurrentPosition(),duration:this.getDuration()}}async startMicrophoneInput(t="default"){this.micEngine&&await this.micEngine.startMicrophoneInput(t)}stopMicrophoneInput(){this.micEngine&&this.micEngine.stopMicrophoneInput()}setAutoTuneSettings(t){if(this.micEngine&&(this.micEngine.setAutoTuneSettings(t),this.micEngine.microphoneGain&&Object.hasOwn(t,"enabled"))){const i=this.mixerState.mic.muted?0:this.dbToLinear(this.mixerState.mic.gain);this.micEngine.setMicrophoneGain(i)}}setMicrophoneGain(t){this.micEngine&&this.micEngine.setMicrophoneGain(t)}setIEMMonoVocals(t){return this.mixerState.iemMonoVocals=t,this.isPlaying?(this.stopAllSources(),this.createAudioGraph(),this.startAudioSources()):this.createAudioGraph(),!0}async loadMicSettings(){try{if(window.kaiAPI.settings&&this.micEngine){const t=await window.kaiAPI.settings.get("micToSpeakers",!1),i=await window.kaiAPI.settings.get("enableMic",!0),e=await window.kaiAPI.settings.get("iemMonoVocals",!0);this.mixerState.micToSpeakers=t,this.mixerState.enableMic=i,this.mixerState.iemMonoVocals=e,this.micEngine.micToSpeakers=t,this.micEngine.enableMic=i;const s=await window.kaiAPI.settings.get("autoTunePreferences",{});if(s.enabled!==void 0&&(this.micEngine.autotuneSettings.enabled=s.enabled),s.strength!==void 0&&(this.micEngine.autotuneSettings.strength=s.strength),s.speed!==void 0&&(this.micEngine.autotuneSettings.speed=s.speed),s.preferVocals!==void 0&&(this.micEngine.autotuneSettings.preferVocals=s.preferVocals),this.micEngine.autotuneSettings.enabled&&this.micEngine.microphoneGain&&this.micEngine.autoTuneWorkletsLoaded){this.micEngine.enableAutoTune();const n=this.mixerState.mic.muted?0:this.dbToLinear(this.mixerState.mic.gain);this.micEngine.setMicrophoneGain(n)}}}catch(t){console.error("Failed to load mic/autotune settings:",t)}}setMicToSpeakers(t){if(this.mixerState.micToSpeakers=t,this.micEngine&&(this.micEngine.setMicToSpeakers(t),this.micEngine.microphoneGain)){const i=this.mixerState.mic.muted?0:this.dbToLinear(this.mixerState.mic.gain);this.micEngine.setMicrophoneGain(i)}}async setEnableMic(t){if(this.mixerState.enableMic=t,this.micEngine&&(await this.micEngine.setEnableMic(t),t&&this.micEngine.microphoneGain)){const i=this.mixerState.mic.muted?0:this.dbToLinear(this.mixerState.mic.gain);this.micEngine.setMicrophoneGain(i)}}stop(){this.isPlaying=!1,this.stopAllSources(),this.stopMicrophoneInput(),this.stopSongEndMonitoring(),this.audioContexts.PA&&(this.audioContexts.PA.close(),this.audioContexts.PA=null),this.audioContexts.IEM&&(this.audioContexts.IEM.close(),this.audioContexts.IEM=null),this.audioBuffers.clear(),this.outputNodes.PA.gainNodes.clear(),this.outputNodes.IEM.gainNodes.clear()}async reinitialize(){this.stop(),await new Promise(t=>setTimeout(t,200)),await this.initialize()}setOnSongEndedCallback(t){this.onSongEndedCallback=t}checkForSongEnd(){const t=this.getDuration(),i=this.getCurrentPosition(),e=this.monitoringStartTime?this.audioContexts.PA.currentTime-this.monitoringStartTime:0;this.isPlaying&&e>2&&t>3&&i>=t-.2&&(this.stopAllSources(),this.stopSongEndMonitoring(),this.pause(),this._triggerSongEnd())}startSongEndMonitoring(){this.songEndMonitor&&clearInterval(this.songEndMonitor),this.monitoringStartTime=this.audioContexts.PA.currentTime,this.songEndMonitor=setInterval(()=>{this.checkForSongEnd()},250)}stopSongEndMonitoring(){this.songEndMonitor&&(clearInterval(this.songEndMonitor),this.songEndMonitor=null)}dbToLinear(t){return Math.pow(10,t/20)}linearToDb(t){return 20*Math.log10(t)}getFormat(){return"kai"}}export{l as KAIPlayer};
2
- //# sourceMappingURL=kaiPlayer-CoMx__a_.js.map
1
+ import{P as d,M as h}from"./microphoneEngine-BaCUhhQc.js";class f extends d{constructor(){super(),this.audioContexts={PA:null,IEM:null},this.outputDevices={PA:"default",IEM:"default"},this.currentPosition=0,this.songData=null,this.audioBuffers=new Map,this.outputNodes={PA:{sourceNodes:new Map,gainNodes:new Map,masterGain:null,analyser:null,vocalsPAGain:null},IEM:{sourceNodes:new Map,gainNodes:new Map,masterGain:null}},this.vocalsPAEnabled=!1,this.startTime=0,this.pauseTime=0,this.micEngine=null,this.mixerState={PA:{gain:0,muted:!1},IEM:{gain:0,muted:!0,mono:!0},mic:{gain:0,muted:!0},stems:[],micToSpeakers:!0,enableMic:!0}}async initialize(){try{const t=await this.getAvailableOutputDevices();await this.loadDevicePreferences(t);const i={};this.outputDevices.PA!=="default"&&"sinkId"in AudioContext.prototype&&(i.sinkId=this.outputDevices.PA),this.audioContexts.PA=new(window.AudioContext||window.webkitAudioContext)(i),this.outputNodes.PA.masterGain=this.audioContexts.PA.createGain(),this.outputNodes.PA.masterGain.connect(this.audioContexts.PA.destination);const e=this.mixerState.PA.muted?0:this.dbToLinear(this.mixerState.PA.gain);this.outputNodes.PA.masterGain.gain.value=e,this.outputNodes.PA.analyser=this.audioContexts.PA.createAnalyser(),this.outputNodes.PA.analyser.fftSize=2048,this.outputNodes.PA.analyser.smoothingTimeConstant=.8,this.outputNodes.PA.vocalsPAGain=this.audioContexts.PA.createGain(),this.outputNodes.PA.vocalsPAGain.gain.value=0,this.outputNodes.PA.vocalsPAGain.connect(this.outputNodes.PA.masterGain);const s={};this.outputDevices.IEM!=="default"&&"sinkId"in AudioContext.prototype&&(s.sinkId=this.outputDevices.IEM),this.audioContexts.IEM=new(window.AudioContext||window.webkitAudioContext)(s),this.outputNodes.IEM.masterGain=this.audioContexts.IEM.createGain(),this.outputNodes.IEM.masterGain.connect(this.audioContexts.IEM.destination);const n=this.mixerState.IEM.muted?0:this.dbToLinear(this.mixerState.IEM.gain);if(this.outputNodes.IEM.masterGain.gain.value=n,this.micEngine=new h(this.audioContexts.PA,this.outputNodes.PA.masterGain,{getCurrentPosition:()=>this.getCurrentPosition()}),await this.micEngine.loadAutoTuneWorklet(),await this.loadMicSettings(),this.micEngine.enableMic){await this.micEngine.startMicrophoneInput(this.micEngine.inputDevice);const a=this.mixerState.mic.muted?0:this.dbToLinear(this.mixerState.mic.gain);this.micEngine.setMicrophoneGain(a)}return!0}catch(t){return console.warn("⚠️ Audio initialization issue:",t.message),!1}}resolveDeviceByLabel(t,i,e){if(!i||!i.id&&!i.name||i.deviceKind==="default"||i.id==="")return"default";if(i.name){const s=t.find(n=>n.label===i.name);if(s)return s.deviceId!==i.id&&console.log(`🎧 ${e}: Matched "${i.name}" by label (ID changed)`),s.deviceId}if(i.id){const s=t.find(n=>n.deviceId===i.id);if(s)return s.deviceId}return console.warn(`🎧 ${e}: Saved device "${i.name||i.id}" not found, using default`),"default"}async loadDevicePreferences(t=[]){try{if(window.kaiAPI.settings){const i=await window.kaiAPI.settings.get("devicePreferences",null);if(i?.PA&&(this.outputDevices.PA=this.resolveDeviceByLabel(t,i.PA,"PA")),i?.IEM&&(this.outputDevices.IEM=this.resolveDeviceByLabel(t,i.IEM,"IEM")),i?.input){const e=await this.getAvailableInputDevices();this.inputDevice=this.resolveDeviceByLabel(e,i.input,"input")}}if(window.kaiAPI?.app){const i=await window.kaiAPI.app.getState();i?.mixer&&(typeof i.mixer.PA?.gain=="number"&&(this.mixerState.PA.gain=i.mixer.PA.gain),typeof i.mixer.PA?.muted=="boolean"&&(this.mixerState.PA.muted=i.mixer.PA.muted),typeof i.mixer.IEM?.gain=="number"&&(this.mixerState.IEM.gain=i.mixer.IEM.gain),typeof i.mixer.IEM?.muted=="boolean"&&(this.mixerState.IEM.muted=i.mixer.IEM.muted),typeof i.mixer.mic?.gain=="number"&&(this.mixerState.mic.gain=i.mixer.mic.gain),typeof i.mixer.mic?.muted=="boolean"&&(this.mixerState.mic.muted=i.mixer.mic.muted))}}catch(i){console.warn("Failed to load device preferences:",i.message)}}async getAvailableOutputDevices(){try{return(await navigator.mediaDevices.enumerateDevices()).filter(i=>i.kind==="audiooutput")}catch(t){return console.warn("Failed to enumerate output devices:",t.message),[]}}async getAvailableInputDevices(){try{return(await navigator.mediaDevices.enumerateDevices()).filter(i=>i.kind==="audioinput")}catch(t){return console.warn("Failed to enumerate input devices:",t.message),[]}}async setOutputDevice(t,i){try{if(!["PA","IEM"].includes(t))return console.error("Invalid bus type:",t),!1;const e=this.isPlaying;e&&this.pause();const s=this.currentPosition;this.outputDevices[t]=i,this.audioContexts[t]&&await this.audioContexts[t].close();const n={};i!=="default"&&"sinkId"in AudioContext.prototype&&(n.sinkId=i),this.audioContexts[t]=new(window.AudioContext||window.webkitAudioContext)(n),this.outputNodes[t].masterGain=this.audioContexts[t].createGain(),this.outputNodes[t].masterGain.connect(this.audioContexts[t].destination);const a=this.mixerState[t].muted?0:this.dbToLinear(this.mixerState[t].gain);if(this.outputNodes[t].masterGain.gain.value=a,t==="PA"&&(this.outputNodes.PA.analyser=this.audioContexts.PA.createAnalyser(),this.outputNodes.PA.analyser.fftSize=2048,this.outputNodes.PA.analyser.smoothingTimeConstant=.8,this.outputNodes.PA.vocalsPAGain=this.audioContexts.PA.createGain(),this.outputNodes.PA.vocalsPAGain.gain.value=this.vocalsPAEnabled?this.dbToLinear(this.mixerState.IEM.gain):0,this.outputNodes.PA.vocalsPAGain.connect(this.outputNodes.PA.masterGain)),this.outputNodes[t].sourceNodes.clear(),this.outputNodes[t].gainNodes.clear(),t==="PA"&&this.micEngine&&(this.micEngine.updateAudioContext(this.audioContexts.PA,this.outputNodes.PA.masterGain),await this.micEngine.loadAutoTuneWorklet(),this.micEngine.enableMic)){await this.micEngine.startMicrophoneInput(this.micEngine.inputDevice);const c=this.mixerState.mic.muted?0:this.dbToLinear(this.mixerState.mic.gain);this.micEngine.setMicrophoneGain(c)}return this.songData&&await this.reloadAudioBuffersForBus(t),e&&this.songData&&(this.currentPosition=s,await this.play()),!0}catch(e){return console.error(`Failed to set ${t} output device:`,e),!1}}async loadSong(t){return this.songData=t,this.resetPosition(),this.currentPosition=0,this.startTime=0,this.pauseTime=0,this.monitoringStartTime=null,this.stopSongEndMonitoring(),this.mixerState.stems=(t.audio?.sources||[]).map((i,e)=>({id:i.name||i.filename,name:i.name||i.filename,gain:i.gain||0,index:e})),await this.loadAudioBuffers(t),this.micEngine&&this.micEngine.clearPitchReference(),this.reportSongLoaded(),this.reportStateChange(),!0}reportSongLoaded(){if(window.kaiAPI?.renderer&&this.songData){const t=this.getDuration();window.kaiAPI.renderer.songLoaded({path:this.songData.originalFilePath||this.songData.filePath,title:this.songData.metadata?.title||"Unknown",artist:this.songData.metadata?.artist||"Unknown",duration:t,isLoading:!1,format:"kai"}),this.reportMixerState()}}reportMixerState(){window.kaiAPI?.renderer&&this.mixerState&&window.kaiAPI.renderer.updateMixerState(this.mixerState)}async loadAudioBuffers(t){if(!t.audio?.sources){console.warn("No audio sources found in song data");return}(!this.audioContexts.PA||!this.audioContexts.IEM)&&await this.initialize();for(const e of t.audio.sources)try{if(e.audioData&&e.audioData.length>0){const s=e.audioData.buffer.slice(e.audioData.byteOffset,e.audioData.byteOffset+e.audioData.byteLength),n=await this.audioContexts.PA.decodeAudioData(s);this.audioBuffers.set(e.name,n)}else console.warn(`No audio data for source: ${e.name}`)}catch(s){console.error(`Failed to decode audio for ${e.name}:`,s)}let i=0;for(const[e,s]of this.audioBuffers)s.duration>i&&(i=s.duration);i>0?(this.songData.metadata||(this.songData.metadata={}),this.songData.metadata.duration=i):console.warn("No audio buffers loaded, duration remains 0")}async reloadAudioBuffersForBus(t){if(!(!this.songData?.audio?.sources||!this.audioContexts[t])){for(const i of this.songData.audio.sources)if(i.audioData&&i.audioData.length>0)try{if(!this.audioBuffers.has(i.name)){const e=i.audioData.buffer.slice(i.audioData.byteOffset,i.audioData.byteOffset+i.audioData.byteLength),s=await this.audioContexts[t].decodeAudioData(e);this.audioBuffers.set(i.name,s)}}catch(e){console.error(`Failed to reload audio buffer for ${t} - ${i.name}:`,e)}}}async play(){return this.songData?!this.audioContexts.PA||!this.audioContexts.IEM?(console.error("Audio contexts not initialized"),!1):(this.audioContexts.PA.state==="suspended"&&await this.audioContexts.PA.resume(),this.audioContexts.IEM.state==="suspended"&&await this.audioContexts.IEM.resume(),this.isPlaying=!0,this.stopAllSources(),this.createAudioGraph(),this.startAudioSources(),this.startSongEndMonitoring(),this.startStateReporting(),this.micEngine&&this.micEngine.setPlaying(!0),this.reportStateChange(),!0):(console.error("No song loaded"),!1)}pause(){return this.currentPosition=this.getCurrentPosition(),this.isPlaying=!1,this.audioContexts.PA&&(this.pauseTime=this.audioContexts.PA.currentTime),this.stopAllSources(),this.stopSongEndMonitoring(),this.stopStateReporting(),this.micEngine&&this.micEngine.setPlaying(!1),this.reportStateChange(),!0}seek(t){return this.currentPosition=t,this.isPlaying&&(this.stopAllSources(),this.startAudioSources()),this.reportStateChange(),!0}stopAllSources(){this.outputNodes.PA.sourceNodes.size+this.outputNodes.IEM.sourceNodes.size,this.outputNodes.PA.sourceNodes.forEach((t,i)=>{try{t.stop(),t.disconnect()}catch{}}),this.outputNodes.PA.sourceNodes.clear(),this.outputNodes.IEM.sourceNodes.forEach((t,i)=>{try{t.stop(),t.disconnect()}catch{}}),this.outputNodes.IEM.sourceNodes.clear()}createAudioGraph(){!this.audioContexts.PA||!this.audioContexts.IEM||(this.outputNodes.PA.gainNodes.clear(),this.outputNodes.IEM.gainNodes.clear(),this.mixerState.stems.forEach(t=>{const i=this.audioContexts.PA.createGain();i.connect(this.outputNodes.PA.masterGain),this.outputNodes.PA.gainNodes.set(t.name,i);const e=this.audioContexts.IEM.createGain();if(this.isVocalStem(t.name)&&this.mixerState.iemMonoVocals){const s=this.audioContexts.IEM.createChannelMerger(1);e.connect(s),s.connect(this.outputNodes.IEM.masterGain)}else e.connect(this.outputNodes.IEM.masterGain);this.outputNodes.IEM.gainNodes.set(t.name,e),this.updateStemGain(t)}))}startAudioSources(){if(!this.audioContexts.PA||!this.audioContexts.IEM)return;const t=.1,i=this.audioContexts.PA.currentTime+t,e=this.audioContexts.IEM.currentTime+t;this.startTime=i,this.mixerState.stems.forEach(s=>{if(this.isMixdownStem(s.name)){console.log(`⏭️ Skipping mixdown stem: ${s.name}`);return}const n=this.audioBuffers.get(s.name),a=this.outputNodes.PA.gainNodes.get(s.name),c=this.outputNodes.IEM.gainNodes.get(s.name);if(n&&a&&c)try{const r=Math.min(this.currentPosition,n.duration);if(this.isVocalStem(s.name)){const o=this.audioContexts.IEM.createBufferSource();if(o.buffer=n,o.connect(c),o.start(e,r),this.outputNodes.IEM.sourceNodes.set(s.name,o),this.outputNodes.PA.vocalsPAGain){const u=this.audioContexts.PA.createBufferSource();u.buffer=n,u.connect(this.outputNodes.PA.vocalsPAGain),this.micEngine&&this.micEngine.connectMusicSource(u),u.start(i,r),this.outputNodes.PA.sourceNodes.set(s.name+"_vocalsPA",u)}}else{const o=this.audioContexts.PA.createBufferSource();o.buffer=n,o.connect(a),this.outputNodes.PA.analyser&&o.connect(this.outputNodes.PA.analyser),this.isMelodicStem(s.name)&&this.micEngine&&this.micEngine.connectMusicSource(o),o.start(i,r),this.outputNodes.PA.sourceNodes.set(s.name,o),o.onended=()=>{this.isPlaying&&setTimeout(()=>this.checkForSongEnd(),10)}}}catch(r){console.error(`Failed to start source for ${s.name}:`,r)}else console.warn(`No audio buffer or gain nodes for stem: ${s.name}`)})}isVocalStem(t){const i=["vocals","vocal","voice","lead","singing","vox"],e=t.toLowerCase();return i.some(s=>e.includes(s))}isMixdownStem(t){const i=["mixdown","mix","master","full mix","stereo mix"],e=t.toLowerCase();return i.some(s=>e===s||e.includes(`_${s}`)||e.includes(`${s}_`))}isMelodicStem(t){const i=t.toLowerCase();return i.includes("other")||i.includes("music")||i.includes("instrumental")||i.includes("accompaniment")||i.includes("melody")?!0:!(this.isVocalStem(t)||i.includes("drum")||i.includes("percussion")||i.includes("bass"))}updateStemGain(t){const i=this.outputNodes.PA.gainNodes.get(t.name),e=this.outputNodes.IEM.gainNodes.get(t.name),s=this.isVocalStem(t.name);if(!this.audioContexts.PA||!this.audioContexts.IEM)return;const n=Math.pow(10,t.gain/20);s&&e?e.gain.setValueAtTime(n,this.audioContexts.IEM.currentTime):!s&&i&&i.gain.setValueAtTime(n,this.audioContexts.PA.currentTime)}setMasterGain(t,i){if(!["PA","IEM","mic"].includes(t))return!1;if(this.mixerState[t].gain=i,t==="PA"&&this.outputNodes.PA.masterGain){const e=this.dbToLinear(i);this.outputNodes.PA.masterGain.gain.setValueAtTime(e,this.audioContexts.PA.currentTime)}else if(t==="IEM"&&this.outputNodes.IEM.masterGain){const e=this.dbToLinear(i);this.outputNodes.IEM.masterGain.gain.setValueAtTime(e,this.audioContexts.IEM.currentTime)}else if(t==="mic"&&this.micEngine){const e=this.mixerState.mic.muted?0:this.dbToLinear(i);this.micEngine.setMicrophoneGain(e)}return this.reportMixerState(),!0}toggleMasterMute(t){if(!["PA","IEM","mic"].includes(t))return!1;this.mixerState[t].muted=!this.mixerState[t].muted;const i=this.mixerState[t].muted;if(t==="PA"&&this.outputNodes.PA.masterGain){const e=i?0:this.dbToLinear(this.mixerState.PA.gain);this.outputNodes.PA.masterGain.gain.setValueAtTime(e,this.audioContexts.PA.currentTime)}else if(t==="IEM"&&this.outputNodes.IEM.masterGain){const e=i?0:this.dbToLinear(this.mixerState.IEM.gain);this.outputNodes.IEM.masterGain.gain.setValueAtTime(e,this.audioContexts.IEM.currentTime)}else if(t==="mic"&&this.micEngine){const e=i?0:this.dbToLinear(this.mixerState.mic.gain);this.micEngine.setMicrophoneGain(e)}return this.reportMixerState(),!0}setMasterMute(t,i){if(!["PA","IEM","mic"].includes(t))return!1;if(this.mixerState[t].muted=i,t==="PA"&&this.outputNodes.PA.masterGain){const e=i?0:this.dbToLinear(this.mixerState.PA.gain);this.outputNodes.PA.masterGain.gain.setValueAtTime(e,this.audioContexts.PA.currentTime)}else if(t==="IEM"&&this.outputNodes.IEM.masterGain){const e=i?0:this.dbToLinear(this.mixerState.IEM.gain);this.outputNodes.IEM.masterGain.gain.setValueAtTime(e,this.audioContexts.IEM.currentTime)}else if(t==="mic"&&this.micEngine){const e=i?0:this.dbToLinear(this.mixerState.mic.gain);this.micEngine.setMicrophoneGain(e)}return!0}setVocalsPAEnabled(t,i=.05){if(!this.outputNodes.PA.vocalsPAGain||!this.audioContexts.PA||this.vocalsPAEnabled===t)return;this.vocalsPAEnabled=t;const e=t?this.dbToLinear(this.mixerState.IEM.gain):0,s=this.audioContexts.PA.currentTime;this.outputNodes.PA.vocalsPAGain.gain.cancelScheduledValues(s),this.outputNodes.PA.vocalsPAGain.gain.linearRampToValueAtTime(e,s+i)}getCurrentPosition(){if(this.isPlaying&&this.audioContexts.PA&&this.startTime>0){const t=this.audioContexts.PA.currentTime-this.startTime,i=this.currentPosition+t,e=this.getDuration();return e>0?Math.min(i,e):i}return this.currentPosition}getCurrentTime(){return this.getCurrentPosition()}getDuration(){return this.songData?.metadata?.duration||0}getMixerState(){return{PA:this.mixerState.PA,IEM:this.mixerState.IEM,mic:this.mixerState.mic,stems:this.mixerState.stems,isPlaying:this.isPlaying,position:this.getCurrentPosition(),duration:this.getDuration()}}async startMicrophoneInput(t="default"){this.micEngine&&await this.micEngine.startMicrophoneInput(t)}stopMicrophoneInput(){this.micEngine&&this.micEngine.stopMicrophoneInput()}setAutoTuneSettings(t){if(this.micEngine&&(this.micEngine.setAutoTuneSettings(t),this.micEngine.microphoneGain&&Object.hasOwn(t,"enabled"))){const i=this.mixerState.mic.muted?0:this.dbToLinear(this.mixerState.mic.gain);this.micEngine.setMicrophoneGain(i)}}setMicrophoneGain(t){this.micEngine&&this.micEngine.setMicrophoneGain(t)}setIEMMonoVocals(t){return this.mixerState.iemMonoVocals=t,this.isPlaying?(this.stopAllSources(),this.createAudioGraph(),this.startAudioSources()):this.createAudioGraph(),!0}async loadMicSettings(){try{if(window.kaiAPI.settings&&this.micEngine){const t=await window.kaiAPI.settings.get("micToSpeakers",!1),i=await window.kaiAPI.settings.get("enableMic",!0),e=await window.kaiAPI.settings.get("iemMonoVocals",!0);this.mixerState.micToSpeakers=t,this.mixerState.enableMic=i,this.mixerState.iemMonoVocals=e,this.micEngine.micToSpeakers=t,this.micEngine.enableMic=i;const s=await window.kaiAPI.settings.get("autoTunePreferences",{});if(s.enabled!==void 0&&(this.micEngine.autotuneSettings.enabled=s.enabled),s.strength!==void 0&&(this.micEngine.autotuneSettings.strength=s.strength),s.speed!==void 0&&(this.micEngine.autotuneSettings.speed=s.speed),s.preferVocals!==void 0&&(this.micEngine.autotuneSettings.preferVocals=s.preferVocals),this.micEngine.autotuneSettings.enabled&&this.micEngine.microphoneGain&&this.micEngine.autoTuneWorkletsLoaded){this.micEngine.enableAutoTune();const n=this.mixerState.mic.muted?0:this.dbToLinear(this.mixerState.mic.gain);this.micEngine.setMicrophoneGain(n)}}}catch(t){console.error("Failed to load mic/autotune settings:",t)}}setMicToSpeakers(t){if(this.mixerState.micToSpeakers=t,this.micEngine&&(this.micEngine.setMicToSpeakers(t),this.micEngine.microphoneGain)){const i=this.mixerState.mic.muted?0:this.dbToLinear(this.mixerState.mic.gain);this.micEngine.setMicrophoneGain(i)}}async setEnableMic(t){if(this.mixerState.enableMic=t,this.micEngine&&(await this.micEngine.setEnableMic(t),t&&this.micEngine.microphoneGain)){const i=this.mixerState.mic.muted?0:this.dbToLinear(this.mixerState.mic.gain);this.micEngine.setMicrophoneGain(i)}}stop(){this.isPlaying=!1,this.stopAllSources(),this.stopMicrophoneInput(),this.stopSongEndMonitoring(),this.audioContexts.PA&&(this.audioContexts.PA.close(),this.audioContexts.PA=null),this.audioContexts.IEM&&(this.audioContexts.IEM.close(),this.audioContexts.IEM=null),this.audioBuffers.clear(),this.outputNodes.PA.gainNodes.clear(),this.outputNodes.IEM.gainNodes.clear()}async reinitialize(){this.stop(),await new Promise(t=>setTimeout(t,200)),await this.initialize()}setOnSongEndedCallback(t){this.onSongEndedCallback=t}checkForSongEnd(){const t=this.getDuration(),i=this.getCurrentPosition(),e=this.monitoringStartTime?this.audioContexts.PA.currentTime-this.monitoringStartTime:0;this.isPlaying&&e>2&&t>3&&i>=t-.2&&(this.stopAllSources(),this.stopSongEndMonitoring(),this.pause(),this._triggerSongEnd())}startSongEndMonitoring(){this.songEndMonitor&&clearInterval(this.songEndMonitor),this.monitoringStartTime=this.audioContexts.PA.currentTime,this.songEndMonitor=setInterval(()=>{this.checkForSongEnd()},250)}stopSongEndMonitoring(){this.songEndMonitor&&(clearInterval(this.songEndMonitor),this.songEndMonitor=null)}dbToLinear(t){return Math.pow(10,t/20)}linearToDb(t){return 20*Math.log10(t)}getFormat(){return"kai"}}export{f as KAIPlayer};
2
+ //# sourceMappingURL=kaiPlayer-DSaY7TxC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kaiPlayer-DSaY7TxC.js","sources":["../../js/kaiPlayer.js"],"sourcesContent":["import { PlayerInterface } from './PlayerInterface.js';\nimport { MicrophoneEngine } from './microphoneEngine.js';\n\nexport class KAIPlayer extends PlayerInterface {\n constructor() {\n super(); // Call PlayerInterface constructor\n\n // Dual audio contexts for PA and IEM outputs\n this.audioContexts = {\n PA: null,\n IEM: null,\n };\n\n // Device IDs for PA and IEM outputs\n this.outputDevices = {\n PA: 'default',\n IEM: 'default',\n };\n\n // Note: this.isPlaying is inherited from PlayerInterface\n this.currentPosition = 0;\n this.songData = null;\n this.audioBuffers = new Map();\n\n // Separate gain nodes and sources for each output\n this.outputNodes = {\n PA: {\n sourceNodes: new Map(),\n gainNodes: new Map(),\n masterGain: null,\n analyser: null, // For butterchurn visualization\n vocalsPAGain: null, // For backup:PA feature - vocals to PA routing\n },\n IEM: {\n sourceNodes: new Map(),\n gainNodes: new Map(),\n masterGain: null,\n },\n };\n\n // Track whether vocals should play through PA (for backup:PA lines)\n this.vocalsPAEnabled = false;\n\n this.startTime = 0;\n this.pauseTime = 0;\n\n // Note: this.onSongEndedCallback is inherited from PlayerInterface\n\n // Microphone engine (handles all mic/auto-tune functionality)\n this.micEngine = null; // Will be initialized after audio contexts are created\n\n this.mixerState = {\n // Simple 3-fader mixer\n PA: {\n gain: 0, // dB\n muted: false,\n },\n IEM: {\n gain: 0, // dB\n muted: true, // Default muted - user must explicitly enable IEM monitoring\n mono: true, // Always mono for single-ear monitoring\n },\n mic: {\n gain: 0, // dB\n muted: true, // Default muted - user must explicitly enable mic\n },\n // Per-song data (for internal use)\n stems: [],\n // Mic routing settings (deprecated - now in micEngine, kept for compatibility)\n micToSpeakers: true,\n enableMic: true,\n };\n\n // Note: this.stateReportInterval is inherited from PlayerInterface\n }\n\n async initialize() {\n try {\n // Get available devices first for label-based matching\n const availableDevices = await this.getAvailableOutputDevices();\n\n // Load and resolve device preferences (matches by label, falls back to ID)\n await this.loadDevicePreferences(availableDevices);\n\n // Initialize PA audio context with resolved device\n const paContextOptions = {};\n if (this.outputDevices.PA !== 'default' && 'sinkId' in AudioContext.prototype) {\n paContextOptions.sinkId = this.outputDevices.PA;\n }\n this.audioContexts.PA = new (window.AudioContext || window.webkitAudioContext)(\n paContextOptions\n );\n this.outputNodes.PA.masterGain = this.audioContexts.PA.createGain();\n this.outputNodes.PA.masterGain.connect(this.audioContexts.PA.destination);\n // Apply saved PA gain (considering mute state)\n const paGain = this.mixerState.PA.muted ? 0 : this.dbToLinear(this.mixerState.PA.gain);\n this.outputNodes.PA.masterGain.gain.value = paGain;\n\n // Create analyser for butterchurn visualization (before gain affects signal)\n this.outputNodes.PA.analyser = this.audioContexts.PA.createAnalyser();\n this.outputNodes.PA.analyser.fftSize = 2048;\n this.outputNodes.PA.analyser.smoothingTimeConstant = 0.8;\n\n // Create vocalsPAGain node for backup:PA feature (vocals to PA routing, muted by default)\n this.outputNodes.PA.vocalsPAGain = this.audioContexts.PA.createGain();\n this.outputNodes.PA.vocalsPAGain.gain.value = 0; // Muted by default\n this.outputNodes.PA.vocalsPAGain.connect(this.outputNodes.PA.masterGain);\n\n // Initialize IEM audio context with validated device\n const iemContextOptions = {};\n if (this.outputDevices.IEM !== 'default' && 'sinkId' in AudioContext.prototype) {\n iemContextOptions.sinkId = this.outputDevices.IEM;\n }\n this.audioContexts.IEM = new (window.AudioContext || window.webkitAudioContext)(\n iemContextOptions\n );\n this.outputNodes.IEM.masterGain = this.audioContexts.IEM.createGain();\n this.outputNodes.IEM.masterGain.connect(this.audioContexts.IEM.destination);\n // Apply saved IEM gain (considering mute state)\n const iemGain = this.mixerState.IEM.muted ? 0 : this.dbToLinear(this.mixerState.IEM.gain);\n this.outputNodes.IEM.masterGain.gain.value = iemGain;\n\n // Initialize microphone engine\n this.micEngine = new MicrophoneEngine(this.audioContexts.PA, this.outputNodes.PA.masterGain, {\n getCurrentPosition: () => this.getCurrentPosition(),\n });\n\n // Load auto-tune worklets\n await this.micEngine.loadAutoTuneWorklet();\n\n // Load mic settings\n await this.loadMicSettings();\n\n // Start microphone if enabled (after reinitialize, mic needs to be restarted)\n if (this.micEngine.enableMic) {\n await this.micEngine.startMicrophoneInput(this.micEngine.inputDevice);\n\n // Apply saved mic gain and mute state\n const linearGain = this.mixerState.mic.muted\n ? 0\n : this.dbToLinear(this.mixerState.mic.gain);\n this.micEngine.setMicrophoneGain(linearGain);\n }\n\n return true;\n } catch (error) {\n console.warn('⚠️ Audio initialization issue:', error.message);\n return false;\n }\n }\n\n /**\n * Find a device by matching label first, then ID as fallback\n * @param {MediaDeviceInfo[]} availableDevices - List of available devices\n * @param {Object} savedPref - Saved preference with id and name properties\n * @param {string} busType - Bus type for logging (PA, IEM, input)\n * @returns {string} Resolved device ID or 'default'\n */\n resolveDeviceByLabel(availableDevices, savedPref, busType) {\n if (!savedPref || (!savedPref.id && !savedPref.name)) {\n return 'default';\n }\n\n // Skip if explicitly set to default\n if (savedPref.deviceKind === 'default' || savedPref.id === '') {\n return 'default';\n }\n\n // Try to match by label/name first (most reliable across reconnections)\n if (savedPref.name) {\n const byLabel = availableDevices.find((d) => d.label === savedPref.name);\n if (byLabel) {\n if (byLabel.deviceId !== savedPref.id) {\n console.log(`🎧 ${busType}: Matched \"${savedPref.name}\" by label (ID changed)`);\n }\n return byLabel.deviceId;\n }\n }\n\n // Fall back to ID match\n if (savedPref.id) {\n const byId = availableDevices.find((d) => d.deviceId === savedPref.id);\n if (byId) {\n return byId.deviceId;\n }\n }\n\n // Device not found - use default\n console.warn(\n `🎧 ${busType}: Saved device \"${savedPref.name || savedPref.id}\" not found, using default`\n );\n return 'default';\n }\n\n async loadDevicePreferences(availableOutputDevices = []) {\n try {\n // Load device preferences from settingsAPI\n if (window.kaiAPI.settings) {\n const devicePrefs = await window.kaiAPI.settings.get('devicePreferences', null);\n\n if (devicePrefs?.PA) {\n this.outputDevices.PA = this.resolveDeviceByLabel(\n availableOutputDevices,\n devicePrefs.PA,\n 'PA'\n );\n }\n if (devicePrefs?.IEM) {\n this.outputDevices.IEM = this.resolveDeviceByLabel(\n availableOutputDevices,\n devicePrefs.IEM,\n 'IEM'\n );\n }\n if (devicePrefs?.input) {\n // For input devices, get input device list\n const inputDevices = await this.getAvailableInputDevices();\n this.inputDevice = this.resolveDeviceByLabel(inputDevices, devicePrefs.input, 'input');\n }\n }\n\n // Load mixer state from AppState\n if (window.kaiAPI?.app) {\n const appState = await window.kaiAPI.app.getState();\n\n // Load mixer state from AppState\n if (appState?.mixer) {\n if (typeof appState.mixer.PA?.gain === 'number') {\n this.mixerState.PA.gain = appState.mixer.PA.gain;\n }\n if (typeof appState.mixer.PA?.muted === 'boolean') {\n this.mixerState.PA.muted = appState.mixer.PA.muted;\n }\n if (typeof appState.mixer.IEM?.gain === 'number') {\n this.mixerState.IEM.gain = appState.mixer.IEM.gain;\n }\n if (typeof appState.mixer.IEM?.muted === 'boolean') {\n this.mixerState.IEM.muted = appState.mixer.IEM.muted;\n }\n if (typeof appState.mixer.mic?.gain === 'number') {\n this.mixerState.mic.gain = appState.mixer.mic.gain;\n }\n if (typeof appState.mixer.mic?.muted === 'boolean') {\n this.mixerState.mic.muted = appState.mixer.mic.muted;\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load device preferences:', error.message);\n }\n }\n\n /**\n * Get list of available audio output devices\n * @returns {Promise<MediaDeviceInfo[]>}\n */\n async getAvailableOutputDevices() {\n try {\n const devices = await navigator.mediaDevices.enumerateDevices();\n return devices.filter((d) => d.kind === 'audiooutput');\n } catch (error) {\n console.warn('Failed to enumerate output devices:', error.message);\n return [];\n }\n }\n\n /**\n * Get list of available audio input devices\n * @returns {Promise<MediaDeviceInfo[]>}\n */\n async getAvailableInputDevices() {\n try {\n const devices = await navigator.mediaDevices.enumerateDevices();\n return devices.filter((d) => d.kind === 'audioinput');\n } catch (error) {\n console.warn('Failed to enumerate input devices:', error.message);\n return [];\n }\n }\n\n async setOutputDevice(busType, deviceId) {\n try {\n if (!['PA', 'IEM'].includes(busType)) {\n console.error('Invalid bus type:', busType);\n return false;\n }\n\n const wasPlaying = this.isPlaying;\n\n // Stop current playback if running (pause() will update currentPosition to actual position)\n if (wasPlaying) {\n this.pause();\n }\n\n // Capture position AFTER pause, which has the accurate current position\n const currentPos = this.currentPosition;\n\n // Store the device preference\n this.outputDevices[busType] = deviceId;\n\n // Close existing context for this bus\n if (this.audioContexts[busType]) {\n await this.audioContexts[busType].close();\n }\n\n // Create new context with proper device\n const contextOptions = {};\n if (deviceId !== 'default' && 'sinkId' in AudioContext.prototype) {\n contextOptions.sinkId = deviceId;\n } else {\n // Use default device - no additional config needed\n }\n\n this.audioContexts[busType] = new (window.AudioContext || window.webkitAudioContext)(\n contextOptions\n );\n this.outputNodes[busType].masterGain = this.audioContexts[busType].createGain();\n this.outputNodes[busType].masterGain.connect(this.audioContexts[busType].destination);\n\n // Reapply saved gain (considering mute state)\n const savedGain = this.mixerState[busType].muted\n ? 0\n : this.dbToLinear(this.mixerState[busType].gain);\n this.outputNodes[busType].masterGain.gain.value = savedGain;\n\n // Create analyser for PA (for butterchurn visualization)\n if (busType === 'PA') {\n this.outputNodes.PA.analyser = this.audioContexts.PA.createAnalyser();\n this.outputNodes.PA.analyser.fftSize = 2048;\n this.outputNodes.PA.analyser.smoothingTimeConstant = 0.8;\n\n // Recreate vocalsPAGain node for backup:PA feature\n this.outputNodes.PA.vocalsPAGain = this.audioContexts.PA.createGain();\n this.outputNodes.PA.vocalsPAGain.gain.value = this.vocalsPAEnabled\n ? this.dbToLinear(this.mixerState.IEM.gain)\n : 0;\n this.outputNodes.PA.vocalsPAGain.connect(this.outputNodes.PA.masterGain);\n }\n\n // Clear old audio nodes\n this.outputNodes[busType].sourceNodes.clear();\n this.outputNodes[busType].gainNodes.clear();\n\n // If PA context was recreated, update microphone engine\n if (busType === 'PA' && this.micEngine) {\n // console.log('[AutoTune] PA context recreated, updating microphone engine...');\n this.micEngine.updateAudioContext(this.audioContexts.PA, this.outputNodes.PA.masterGain);\n await this.micEngine.loadAutoTuneWorklet();\n\n // Restart microphone if it was enabled\n if (this.micEngine.enableMic) {\n await this.micEngine.startMicrophoneInput(this.micEngine.inputDevice);\n\n // Reapply saved mic gain and mute state\n const linearGain = this.mixerState.mic.muted\n ? 0\n : this.dbToLinear(this.mixerState.mic.gain);\n this.micEngine.setMicrophoneGain(linearGain);\n }\n }\n\n // Reload audio buffers for the new context (audio buffers are context-specific)\n if (this.songData) {\n await this.reloadAudioBuffersForBus(busType);\n }\n\n // Resume playback if it was playing\n if (wasPlaying && this.songData) {\n this.currentPosition = currentPos;\n await this.play();\n }\n\n return true;\n } catch (error) {\n console.error(`Failed to set ${busType} output device:`, error);\n return false;\n }\n }\n\n async loadSong(songData) {\n this.songData = songData;\n\n // Reset position using base class method\n this.resetPosition();\n\n // Reset engine-specific timing state\n this.currentPosition = 0;\n this.startTime = 0;\n this.pauseTime = 0;\n this.monitoringStartTime = null;\n\n // Stop any existing song end monitoring\n this.stopSongEndMonitoring();\n\n // Keep stems array for tracking audio sources (for internal use only)\n // Routing is automatic: vocals → IEM (mono), instrumental → PA, mic → PA\n this.mixerState.stems = (songData.audio?.sources || []).map((source, index) => ({\n id: source.name || source.filename,\n name: source.name || source.filename,\n gain: source.gain || 0, // Per-source gain (still useful for balancing)\n index,\n }));\n\n await this.loadAudioBuffers(songData);\n\n // Clear any previous pitch reference data\n // Note: Pitch detection is now done in real-time from the vocal stem\n if (this.micEngine) {\n this.micEngine.clearPitchReference();\n }\n\n // Report song loaded to main process\n this.reportSongLoaded();\n\n // Report initial playback state with position reset to 0\n this.reportStateChange();\n\n return true;\n }\n\n /**\n * Note: reportStateChange(), startStateReporting(), and stopStateReporting()\n * are inherited from PlayerInterface base class\n */\n\n reportSongLoaded() {\n if (window.kaiAPI?.renderer && this.songData) {\n const duration = this.getDuration();\n window.kaiAPI.renderer.songLoaded({\n path: this.songData.originalFilePath || this.songData.filePath,\n title: this.songData.metadata?.title || 'Unknown',\n artist: this.songData.metadata?.artist || 'Unknown',\n duration: duration,\n isLoading: false, // Song is fully loaded\n format: 'kai',\n });\n\n // Report initial mixer state\n this.reportMixerState();\n }\n }\n\n reportMixerState() {\n if (window.kaiAPI?.renderer && this.mixerState) {\n window.kaiAPI.renderer.updateMixerState(this.mixerState);\n }\n }\n\n async loadAudioBuffers(songData) {\n if (!songData.audio?.sources) {\n console.warn('No audio sources found in song data');\n return;\n }\n\n if (!this.audioContexts.PA || !this.audioContexts.IEM) {\n await this.initialize();\n }\n\n for (const source of songData.audio.sources) {\n try {\n if (source.audioData && source.audioData.length > 0) {\n const arrayBuffer = source.audioData.buffer.slice(\n source.audioData.byteOffset,\n source.audioData.byteOffset + source.audioData.byteLength\n );\n\n // Sequential audio buffer decoding to avoid overwhelming WebAudio API\n // eslint-disable-next-line no-await-in-loop\n const decodedBuffer = await this.audioContexts.PA.decodeAudioData(arrayBuffer);\n this.audioBuffers.set(source.name, decodedBuffer);\n } else {\n console.warn(`No audio data for source: ${source.name}`);\n }\n } catch (error) {\n console.error(`Failed to decode audio for ${source.name}:`, error);\n }\n }\n\n // Calculate the actual duration from the longest audio buffer\n let maxDuration = 0;\n for (const [_name, buffer] of this.audioBuffers) {\n if (buffer.duration > maxDuration) {\n maxDuration = buffer.duration;\n }\n }\n\n // Update the song metadata with the actual duration\n if (maxDuration > 0) {\n if (!this.songData.metadata) {\n this.songData.metadata = {};\n }\n this.songData.metadata.duration = maxDuration;\n } else {\n console.warn('No audio buffers loaded, duration remains 0');\n }\n }\n\n async reloadAudioBuffersForBus(busType) {\n if (!this.songData?.audio?.sources || !this.audioContexts[busType]) {\n return;\n }\n\n // Audio buffers are shared between contexts, but we need to re-decode for new context\n // The existing buffers in this.audioBuffers should still work, but let's make sure\n // the new context has access to them by re-decoding if needed\n\n for (const source of this.songData.audio.sources) {\n if (source.audioData && source.audioData.length > 0) {\n try {\n // Check if we already have this buffer\n if (!this.audioBuffers.has(source.name)) {\n const arrayBuffer = source.audioData.buffer.slice(\n source.audioData.byteOffset,\n source.audioData.byteOffset + source.audioData.byteLength\n );\n\n // Sequential buffer decoding for new audio context to avoid WebAudio API errors\n // eslint-disable-next-line no-await-in-loop\n const decodedBuffer = await this.audioContexts[busType].decodeAudioData(arrayBuffer);\n this.audioBuffers.set(source.name, decodedBuffer);\n }\n } catch (error) {\n console.error(`Failed to reload audio buffer for ${busType} - ${source.name}:`, error);\n }\n }\n }\n }\n\n async play() {\n if (!this.songData) {\n console.error('No song loaded');\n return false;\n }\n\n if (!this.audioContexts.PA || !this.audioContexts.IEM) {\n console.error('Audio contexts not initialized');\n return false;\n }\n\n if (this.audioContexts.PA.state === 'suspended') {\n await this.audioContexts.PA.resume();\n }\n if (this.audioContexts.IEM.state === 'suspended') {\n await this.audioContexts.IEM.resume();\n }\n\n this.isPlaying = true;\n\n this.stopAllSources();\n this.createAudioGraph();\n this.startAudioSources();\n\n // Start song end monitoring\n this.startSongEndMonitoring();\n\n // Start state reporting\n this.startStateReporting();\n\n // Update microphone engine playing state\n if (this.micEngine) {\n this.micEngine.setPlaying(true);\n }\n\n // Report immediate state change\n this.reportStateChange();\n\n return true;\n }\n\n pause() {\n // Save current playback position BEFORE setting isPlaying to false\n // (getCurrentPosition uses isPlaying to calculate position)\n this.currentPosition = this.getCurrentPosition();\n\n this.isPlaying = false;\n\n if (this.audioContexts.PA) {\n this.pauseTime = this.audioContexts.PA.currentTime;\n }\n\n this.stopAllSources();\n\n // Stop song end monitoring\n this.stopSongEndMonitoring();\n\n // Stop state reporting\n this.stopStateReporting();\n\n // Update microphone engine playing state\n if (this.micEngine) {\n this.micEngine.setPlaying(false);\n }\n\n // Report immediate state change\n this.reportStateChange();\n\n return true;\n }\n\n seek(positionSec) {\n this.currentPosition = positionSec;\n\n if (this.isPlaying) {\n this.stopAllSources();\n this.startAudioSources();\n }\n\n // Report immediate state change\n this.reportStateChange();\n\n return true;\n }\n\n stopAllSources() {\n const _totalSources =\n this.outputNodes.PA.sourceNodes.size + this.outputNodes.IEM.sourceNodes.size;\n\n // Stop PA sources\n this.outputNodes.PA.sourceNodes.forEach((source, _index) => {\n try {\n source.stop();\n source.disconnect(); // Disconnect all connections\n } catch {\n // Source may already be stopped\n }\n });\n this.outputNodes.PA.sourceNodes.clear();\n\n // Stop IEM sources\n this.outputNodes.IEM.sourceNodes.forEach((source, _index) => {\n try {\n source.stop();\n source.disconnect(); // Disconnect all connections\n } catch {\n // Source may already be stopped\n }\n });\n this.outputNodes.IEM.sourceNodes.clear();\n }\n\n createAudioGraph() {\n if (!this.audioContexts.PA || !this.audioContexts.IEM) return;\n\n // Clear existing gain nodes for both outputs\n this.outputNodes.PA.gainNodes.clear();\n this.outputNodes.IEM.gainNodes.clear();\n\n this.mixerState.stems.forEach((stem) => {\n // Create gain node for PA output\n const paGainNode = this.audioContexts.PA.createGain();\n paGainNode.connect(this.outputNodes.PA.masterGain);\n this.outputNodes.PA.gainNodes.set(stem.name, paGainNode);\n\n // Create gain node for IEM output\n const iemGainNode = this.audioContexts.IEM.createGain();\n\n // For vocal stems, add mono conversion if enabled\n if (this.isVocalStem(stem.name) && this.mixerState.iemMonoVocals) {\n // Create channel merger to convert stereo to mono\n const channelMerger = this.audioContexts.IEM.createChannelMerger(1);\n iemGainNode.connect(channelMerger);\n channelMerger.connect(this.outputNodes.IEM.masterGain);\n } else {\n iemGainNode.connect(this.outputNodes.IEM.masterGain);\n }\n\n this.outputNodes.IEM.gainNodes.set(stem.name, iemGainNode);\n\n this.updateStemGain(stem);\n });\n }\n\n startAudioSources() {\n if (!this.audioContexts.PA || !this.audioContexts.IEM) return;\n\n // Schedule per context — each AudioContext has its own local clock, so a single\n // shared scheduleTime would bake in a skew equal to |PA.currentTime - IEM.currentTime|.\n const lead = 0.1;\n const paStart = this.audioContexts.PA.currentTime + lead;\n const iemStart = this.audioContexts.IEM.currentTime + lead;\n this.startTime = paStart; // getCurrentPosition() measures elapsed against PA's clock\n\n this.mixerState.stems.forEach((stem) => {\n // Skip mixdown stems - they contain the full mix and would overlap with individual stems\n if (this.isMixdownStem(stem.name)) {\n console.log(`⏭️ Skipping mixdown stem: ${stem.name}`);\n return;\n }\n\n const audioBuffer = this.audioBuffers.get(stem.name);\n const paGainNode = this.outputNodes.PA.gainNodes.get(stem.name);\n const iemGainNode = this.outputNodes.IEM.gainNodes.get(stem.name);\n\n if (audioBuffer && paGainNode && iemGainNode) {\n try {\n const offset = Math.min(this.currentPosition, audioBuffer.duration);\n const isVocals = this.isVocalStem(stem.name);\n\n // Proper karaoke routing: vocals to IEM only, music/backing tracks to PA only\n // Exception: backup:PA feature routes vocals to PA when enabled\n if (isVocals) {\n // Vocals go to IEM (singer's ears)\n const iemSource = this.audioContexts.IEM.createBufferSource();\n iemSource.buffer = audioBuffer;\n iemSource.connect(iemGainNode);\n iemSource.start(iemStart, offset);\n this.outputNodes.IEM.sourceNodes.set(stem.name, iemSource);\n\n // Also create PA source for backup:PA feature (muted by default via vocalsPAGain)\n if (this.outputNodes.PA.vocalsPAGain) {\n const paSource = this.audioContexts.PA.createBufferSource();\n paSource.buffer = audioBuffer; // Reuse the same decoded buffer\n paSource.connect(this.outputNodes.PA.vocalsPAGain);\n\n // Connect vocals to pitch detection for auto-tune reference\n // This enables real-time pitch tracking from the vocal stem\n if (this.micEngine) {\n this.micEngine.connectMusicSource(paSource);\n }\n\n paSource.start(paStart, offset);\n this.outputNodes.PA.sourceNodes.set(stem.name + '_vocalsPA', paSource);\n }\n } else {\n // Backing tracks go to PA only (audience)\n const paSource = this.audioContexts.PA.createBufferSource();\n paSource.buffer = audioBuffer;\n paSource.connect(paGainNode);\n\n // Connect to analyser for butterchurn visualization (before gain affects signal)\n if (this.outputNodes.PA.analyser) {\n paSource.connect(this.outputNodes.PA.analyser);\n }\n\n // If this is a melodic stem, connect to microphone engine for pitch detection\n if (this.isMelodicStem(stem.name) && this.micEngine) {\n this.micEngine.connectMusicSource(paSource);\n }\n\n paSource.start(paStart, offset);\n this.outputNodes.PA.sourceNodes.set(stem.name, paSource);\n\n // Add onended handler as backup to position monitoring\n paSource.onended = () => {\n if (this.isPlaying) {\n // Let the position monitoring handle the cleanup\n // This serves as a backup in case position monitoring misses it\n setTimeout(() => this.checkForSongEnd(), 10);\n }\n };\n }\n } catch (error) {\n console.error(`Failed to start source for ${stem.name}:`, error);\n }\n } else {\n console.warn(`No audio buffer or gain nodes for stem: ${stem.name}`);\n }\n });\n\n // Debug PA output routing (disabled)\n // this.outputNodes.PA.gainNodes.forEach((gainNode, stemName) => {\n // console.log('PA routing:', stemName, gainNode);\n // });\n }\n\n isVocalStem(stemName) {\n const vocalsKeywords = ['vocals', 'vocal', 'voice', 'lead', 'singing', 'vox'];\n const lowerName = stemName.toLowerCase();\n return vocalsKeywords.some((keyword) => lowerName.includes(keyword));\n }\n\n isMixdownStem(stemName) {\n // Mixdown stems contain the full mix and should be skipped when individual stems are available\n const mixdownKeywords = ['mixdown', 'mix', 'master', 'full mix', 'stereo mix'];\n const lowerName = stemName.toLowerCase();\n return mixdownKeywords.some(\n (keyword) =>\n lowerName === keyword ||\n lowerName.includes(`_${keyword}`) ||\n lowerName.includes(`${keyword}_`)\n );\n }\n\n isMelodicStem(stemName) {\n // Returns true for stems containing melodic instruments (typically \"other\")\n // These are best for pitch detection as melody reference\n const lowerName = stemName.toLowerCase();\n\n // Explicitly melodic stems\n if (\n lowerName.includes('other') ||\n lowerName.includes('music') ||\n lowerName.includes('instrumental') ||\n lowerName.includes('accompaniment') ||\n lowerName.includes('melody')\n ) {\n return true;\n }\n\n // Exclude non-melodic stems\n if (this.isVocalStem(stemName)) return false;\n if (lowerName.includes('drum') || lowerName.includes('percussion')) return false;\n if (lowerName.includes('bass')) return false;\n\n // Default to true for unknown stems (likely melodic)\n return true;\n }\n\n updateStemGain(stem) {\n const paGainNode = this.outputNodes.PA.gainNodes.get(stem.name);\n const iemGainNode = this.outputNodes.IEM.gainNodes.get(stem.name);\n const isVocals = this.isVocalStem(stem.name);\n\n if (!this.audioContexts.PA || !this.audioContexts.IEM) return;\n\n // Convert stem gain from dB to linear (per-stem balancing)\n const baseGain = Math.pow(10, stem.gain / 20);\n\n // Simple routing: vocals to IEM, backing tracks to PA\n // Master faders control overall output level\n if (isVocals && iemGainNode) {\n iemGainNode.gain.setValueAtTime(baseGain, this.audioContexts.IEM.currentTime);\n } else if (!isVocals && paGainNode) {\n paGainNode.gain.setValueAtTime(baseGain, this.audioContexts.PA.currentTime);\n }\n }\n\n // New simple mixer controls\n setMasterGain(bus, gainDb) {\n if (!['PA', 'IEM', 'mic'].includes(bus)) return false;\n\n this.mixerState[bus].gain = gainDb;\n\n // Apply to audio node\n if (bus === 'PA' && this.outputNodes.PA.masterGain) {\n const linearGain = this.dbToLinear(gainDb);\n this.outputNodes.PA.masterGain.gain.setValueAtTime(\n linearGain,\n this.audioContexts.PA.currentTime\n );\n } else if (bus === 'IEM' && this.outputNodes.IEM.masterGain) {\n const linearGain = this.dbToLinear(gainDb);\n this.outputNodes.IEM.masterGain.gain.setValueAtTime(\n linearGain,\n this.audioContexts.IEM.currentTime\n );\n } else if (bus === 'mic' && this.micEngine) {\n // Apply mic gain (considering mute state)\n const linearGain = this.mixerState.mic.muted ? 0 : this.dbToLinear(gainDb);\n this.micEngine.setMicrophoneGain(linearGain);\n }\n\n // Report to main process (which handles persistence via AppState)\n this.reportMixerState();\n return true;\n }\n\n toggleMasterMute(bus) {\n if (!['PA', 'IEM', 'mic'].includes(bus)) return false;\n\n this.mixerState[bus].muted = !this.mixerState[bus].muted;\n const muted = this.mixerState[bus].muted;\n\n // Apply mute (set gain to 0 or restore)\n if (bus === 'PA' && this.outputNodes.PA.masterGain) {\n const gain = muted ? 0 : this.dbToLinear(this.mixerState.PA.gain);\n this.outputNodes.PA.masterGain.gain.setValueAtTime(gain, this.audioContexts.PA.currentTime);\n } else if (bus === 'IEM' && this.outputNodes.IEM.masterGain) {\n const gain = muted ? 0 : this.dbToLinear(this.mixerState.IEM.gain);\n this.outputNodes.IEM.masterGain.gain.setValueAtTime(gain, this.audioContexts.IEM.currentTime);\n } else if (bus === 'mic' && this.micEngine) {\n const gain = muted ? 0 : this.dbToLinear(this.mixerState.mic.gain);\n this.micEngine.setMicrophoneGain(gain);\n }\n\n // Report to main process (which handles persistence via AppState)\n this.reportMixerState();\n return true;\n }\n\n setMasterMute(bus, muted) {\n if (!['PA', 'IEM', 'mic'].includes(bus)) return false;\n\n this.mixerState[bus].muted = muted;\n\n // Apply mute (set gain to 0 or restore)\n if (bus === 'PA' && this.outputNodes.PA.masterGain) {\n const gain = muted ? 0 : this.dbToLinear(this.mixerState.PA.gain);\n this.outputNodes.PA.masterGain.gain.setValueAtTime(gain, this.audioContexts.PA.currentTime);\n } else if (bus === 'IEM' && this.outputNodes.IEM.masterGain) {\n const gain = muted ? 0 : this.dbToLinear(this.mixerState.IEM.gain);\n this.outputNodes.IEM.masterGain.gain.setValueAtTime(gain, this.audioContexts.IEM.currentTime);\n } else if (bus === 'mic' && this.micEngine) {\n const gain = muted ? 0 : this.dbToLinear(this.mixerState.mic.gain);\n this.micEngine.setMicrophoneGain(gain);\n }\n\n // Don't report back to main - this was initiated by main/admin\n return true;\n }\n\n /**\n * Enable/disable vocals routing to PA (for backup:PA feature)\n * When a lyric line has singer=\"backup:PA\", the original vocals should play through PA\n * @param {boolean} enabled - Whether to route vocals to PA\n * @param {number} fadeTime - Fade duration in seconds (default 50ms to avoid clicks)\n */\n setVocalsPAEnabled(enabled, fadeTime = 0.05) {\n if (!this.outputNodes.PA.vocalsPAGain || !this.audioContexts.PA) return;\n\n // Avoid redundant changes\n if (this.vocalsPAEnabled === enabled) return;\n this.vocalsPAEnabled = enabled;\n\n // Use IEM gain as reference for vocals volume (since that's where vocals normally go)\n const targetGain = enabled ? this.dbToLinear(this.mixerState.IEM.gain) : 0;\n\n // Smooth fade to avoid clicks/pops\n const currentTime = this.audioContexts.PA.currentTime;\n this.outputNodes.PA.vocalsPAGain.gain.cancelScheduledValues(currentTime);\n this.outputNodes.PA.vocalsPAGain.gain.linearRampToValueAtTime(\n targetGain,\n currentTime + fadeTime\n );\n }\n\n // Preset system removed - routing is now automatic with master faders\n // Vocals → IEM (mono), Instrumental → PA, Mic → PA\n\n getCurrentPosition() {\n if (this.isPlaying && this.audioContexts.PA && this.startTime > 0) {\n const elapsed = this.audioContexts.PA.currentTime - this.startTime;\n const calculatedPosition = this.currentPosition + elapsed;\n\n // Don't let position exceed song duration\n const duration = this.getDuration();\n const clampedPosition =\n duration > 0 ? Math.min(calculatedPosition, duration) : calculatedPosition;\n\n return clampedPosition;\n }\n return this.currentPosition;\n }\n\n getCurrentTime() {\n return this.getCurrentPosition();\n }\n\n getDuration() {\n return this.songData?.metadata?.duration || 0;\n }\n\n getMixerState() {\n return {\n PA: this.mixerState.PA,\n IEM: this.mixerState.IEM,\n mic: this.mixerState.mic,\n stems: this.mixerState.stems, // For reference only\n isPlaying: this.isPlaying,\n position: this.getCurrentPosition(),\n duration: this.getDuration(),\n };\n }\n\n // Microphone/Auto-tune delegation methods (delegate to MicrophoneEngine)\n\n async startMicrophoneInput(deviceId = 'default') {\n if (this.micEngine) {\n await this.micEngine.startMicrophoneInput(deviceId);\n }\n }\n\n stopMicrophoneInput() {\n if (this.micEngine) {\n this.micEngine.stopMicrophoneInput();\n }\n }\n\n setAutoTuneSettings(settings) {\n if (this.micEngine) {\n this.micEngine.setAutoTuneSettings(settings);\n\n // Reapply gain after auto-tune enable/disable reconnects the audio chain\n if (this.micEngine.microphoneGain && Object.hasOwn(settings, 'enabled')) {\n const linearGain = this.mixerState.mic.muted\n ? 0\n : this.dbToLinear(this.mixerState.mic.gain);\n this.micEngine.setMicrophoneGain(linearGain);\n }\n }\n }\n\n setMicrophoneGain(gainValue) {\n if (this.micEngine) {\n this.micEngine.setMicrophoneGain(gainValue);\n }\n }\n\n setIEMMonoVocals(enabled) {\n this.mixerState.iemMonoVocals = enabled;\n\n // If playing, recreate audio graph to apply the change\n if (this.isPlaying) {\n this.stopAllSources();\n this.createAudioGraph();\n this.startAudioSources();\n } else {\n // Just recreate the graph for next playback\n this.createAudioGraph();\n }\n\n return true;\n }\n\n async loadMicSettings() {\n try {\n if (window.kaiAPI.settings && this.micEngine) {\n const micToSpeakers = await window.kaiAPI.settings.get('micToSpeakers', false);\n const enableMic = await window.kaiAPI.settings.get('enableMic', true);\n const iemMonoVocals = await window.kaiAPI.settings.get('iemMonoVocals', true);\n\n this.mixerState.micToSpeakers = micToSpeakers;\n this.mixerState.enableMic = enableMic;\n this.mixerState.iemMonoVocals = iemMonoVocals;\n\n // Load settings into microphone engine\n this.micEngine.micToSpeakers = micToSpeakers;\n this.micEngine.enableMic = enableMic;\n\n // Load auto-tune settings\n const autoTunePrefs = await window.kaiAPI.settings.get('autoTunePreferences', {});\n if (autoTunePrefs.enabled !== undefined) {\n this.micEngine.autotuneSettings.enabled = autoTunePrefs.enabled;\n }\n if (autoTunePrefs.strength !== undefined) {\n this.micEngine.autotuneSettings.strength = autoTunePrefs.strength;\n }\n if (autoTunePrefs.speed !== undefined) {\n this.micEngine.autotuneSettings.speed = autoTunePrefs.speed;\n }\n if (autoTunePrefs.preferVocals !== undefined) {\n this.micEngine.autotuneSettings.preferVocals = autoTunePrefs.preferVocals;\n }\n\n // console.log('[AutoTune] Loaded settings:', this.micEngine.autotuneSettings);\n\n // If auto-tune is enabled and mic is already running, apply it\n if (\n this.micEngine.autotuneSettings.enabled &&\n this.micEngine.microphoneGain &&\n this.micEngine.autoTuneWorkletsLoaded\n ) {\n // console.log('[AutoTune] Applying enabled auto-tune from saved settings');\n this.micEngine.enableAutoTune();\n\n // Reapply gain after auto-tune reconnects the audio chain\n const linearGain = this.mixerState.mic.muted\n ? 0\n : this.dbToLinear(this.mixerState.mic.gain);\n this.micEngine.setMicrophoneGain(linearGain);\n }\n }\n } catch (error) {\n console.error('Failed to load mic/autotune settings:', error);\n }\n }\n\n setMicToSpeakers(enabled) {\n this.mixerState.micToSpeakers = enabled;\n if (this.micEngine) {\n this.micEngine.setMicToSpeakers(enabled);\n\n // Reapply gain after mic routing changes reconnect the audio chain\n if (this.micEngine.microphoneGain) {\n const linearGain = this.mixerState.mic.muted\n ? 0\n : this.dbToLinear(this.mixerState.mic.gain);\n this.micEngine.setMicrophoneGain(linearGain);\n }\n }\n }\n\n async setEnableMic(enabled) {\n this.mixerState.enableMic = enabled;\n if (this.micEngine) {\n await this.micEngine.setEnableMic(enabled);\n\n // Reapply saved mic gain and mute state after mic restarts\n if (enabled && this.micEngine.microphoneGain) {\n const linearGain = this.mixerState.mic.muted\n ? 0\n : this.dbToLinear(this.mixerState.mic.gain);\n this.micEngine.setMicrophoneGain(linearGain);\n }\n }\n }\n\n stop() {\n this.isPlaying = false;\n this.stopAllSources();\n this.stopMicrophoneInput();\n this.stopSongEndMonitoring();\n\n if (this.audioContexts.PA) {\n this.audioContexts.PA.close();\n this.audioContexts.PA = null;\n }\n if (this.audioContexts.IEM) {\n this.audioContexts.IEM.close();\n this.audioContexts.IEM = null;\n }\n\n this.audioBuffers.clear();\n this.outputNodes.PA.gainNodes.clear();\n this.outputNodes.IEM.gainNodes.clear();\n }\n\n async reinitialize() {\n this.stop();\n\n // Wait for audio sources to fully stop and contexts to close\n await new Promise((resolve) => setTimeout(resolve, 200));\n\n await this.initialize();\n }\n\n setOnSongEndedCallback(callback) {\n this.onSongEndedCallback = callback;\n }\n\n // Check if song has ended based on position\n checkForSongEnd() {\n const duration = this.getDuration();\n const currentPos = this.getCurrentPosition();\n const timeSinceMonitoringStarted = this.monitoringStartTime\n ? this.audioContexts.PA.currentTime - this.monitoringStartTime\n : 0;\n\n // Only trigger if:\n // 1. We've been monitoring for at least 2 seconds (prevents seek issues)\n // 2. Song duration > 3 seconds\n // 3. Current position is near the end\n if (\n this.isPlaying &&\n timeSinceMonitoringStarted > 2.0 &&\n duration > 3 &&\n currentPos >= duration - 0.2\n ) {\n this.stopAllSources();\n this.stopSongEndMonitoring();\n\n // Pause to properly clean up (sets isPlaying = false, updates renderer, etc.)\n this.pause();\n\n // Use base class method for consistent song end handling (triggers callback)\n this._triggerSongEnd();\n }\n }\n\n // Start monitoring for song end\n startSongEndMonitoring() {\n if (this.songEndMonitor) {\n clearInterval(this.songEndMonitor);\n }\n\n // Track when monitoring actually started to prevent false positives\n this.monitoringStartTime = this.audioContexts.PA.currentTime;\n\n this.songEndMonitor = setInterval(() => {\n this.checkForSongEnd();\n }, 250); // Check every 250ms for more responsive detection\n }\n\n // Stop monitoring for song end\n stopSongEndMonitoring() {\n if (this.songEndMonitor) {\n clearInterval(this.songEndMonitor);\n this.songEndMonitor = null;\n }\n }\n\n // Utility: Convert dB to linear gain\n dbToLinear(db) {\n return Math.pow(10, db / 20);\n }\n\n // Utility: Convert linear gain to dB\n linearToDb(linear) {\n return 20 * Math.log10(linear);\n }\n\n /**\n * Get the format type this player handles\n * @returns {string} Format name\n */\n getFormat() {\n return 'kai';\n }\n}\n\n// Export removed - KAIPlayer is instantiated by KaiPlayerApp in main.js\n// No longer attached to window global\n"],"names":["KAIPlayer","PlayerInterface","availableDevices","paContextOptions","paGain","iemContextOptions","iemGain","MicrophoneEngine","linearGain","error","savedPref","busType","byLabel","d","byId","availableOutputDevices","devicePrefs","inputDevices","appState","deviceId","wasPlaying","currentPos","contextOptions","savedGain","songData","source","index","duration","arrayBuffer","decodedBuffer","maxDuration","_name","buffer","positionSec","_index","stem","paGainNode","iemGainNode","channelMerger","lead","paStart","iemStart","audioBuffer","offset","iemSource","paSource","stemName","vocalsKeywords","lowerName","keyword","mixdownKeywords","isVocals","baseGain","bus","gainDb","muted","gain","enabled","fadeTime","targetGain","currentTime","elapsed","calculatedPosition","settings","gainValue","micToSpeakers","enableMic","iemMonoVocals","autoTunePrefs","resolve","callback","timeSinceMonitoringStarted","db","linear"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ async function P(e,a,n){if(e.player.currentFormat="cdg",e.player.currentPlayer=e.player.cdgPlayer,e.player.currentPlayer.onSongEnded(()=>e.handleSongEnded()),!e.kaiPlayer){console.error("💿 Audio engine not initialized"),e.updateStatus("Error: Audio engine not ready");return}const r=e.kaiPlayer.audioContexts.PA,t=e.kaiPlayer.outputNodes.PA.masterGain,i=r.createGain();i.connect(t);const l=r.createAnalyser();if(l.fftSize=2048,i.connect(l),e.player.cdgPlayer.setAudioContext(r,i,l),await e.player.cdgPlayer.loadSong(a),e.player.cdgPlayer.micEngine){const y=await window.kaiAPI.settings.get("micToSpeakers",!0),u=await window.kaiAPI.settings.get("enableMic",!0),o=await window.kaiAPI.settings.get("autoTunePreferences",{});if(e.player.cdgPlayer.micEngine.micToSpeakers=y,e.player.cdgPlayer.micEngine.enableMic=u,o.enabled!==void 0&&(e.player.cdgPlayer.micEngine.autotuneSettings.enabled=o.enabled),o.strength!==void 0&&(e.player.cdgPlayer.micEngine.autotuneSettings.strength=o.strength),o.speed!==void 0&&(e.player.cdgPlayer.micEngine.autotuneSettings.speed=o.speed),u){const f=(await window.kaiAPI.settings.get("devicePreferences",{}))?.input?.id||"default";await e.player.cdgPlayer.startMicrophoneInput(f)}}const s=await window.kaiAPI.settings.get("waveformPreferences",{enableEffects:!0,randomEffectOnSong:!1,overlayOpacity:.7});await e.player.karaokeRenderer.ensureInputDeviceSelection(),e.player.cdgPlayer.setOverlayOpacity(s.overlayOpacity),e.player.cdgPlayer.setEffectsEnabled(s.enableEffects),await g(e,a,s);const d={...n,requester:n.requester||a.requester||e.currentSong?.requester};e.player.onSongLoaded(d),window.kaiAPI?.renderer&&window.kaiAPI.renderer.songLoaded({path:e.currentSong?.originalFilePath||e.currentSong?.filePath,metadata:n,isLoading:!1,title:n?.title||"CDG Song",artist:n?.artist||"Unknown Artist",format:"cdg",duration:e.player.cdgPlayer?.getDuration()||0,requester:e.currentSong?.requester||"KJ"}),e.updateStatus(`Loaded: ${n?.title||"CDG Song"}`)}async function S(e,a,n){if(e.player.currentFormat="kai",e.player.currentPlayer=e.kaiPlayer,e.player.currentPlayer.onSongEnded(()=>e.handleSongEnded()),e.kaiPlayer&&e.currentSong){const r={...e.currentSong,audio:e.currentSong.audio?{...e.currentSong.audio,sources:e.currentSong.audio.sources?[...e.currentSong.audio.sources]:[]}:null};await e.kaiPlayer.reinitialize(),await e.kaiPlayer.loadSong(r),!e.currentSong.audio&&r.audio&&(e.currentSong.audio=r.audio),!e.currentSong.lyrics&&r.lyrics&&(e.currentSong.lyrics=r.lyrics)}if(e.player&&e.currentSong){e.player.karaokeRenderer&&e.player.karaokeRenderer.reinitialize();const r={...n,lyrics:e.currentSong.lyrics,duration:e.kaiPlayer?e.kaiPlayer.getDuration():e.currentSong.metadata?.duration||0,audio:e.currentSong.audio,requester:n.requester||a.requester||e.currentSong.requester};e.player.onSongLoaded(r);const t=await window.kaiAPI.settings.get("waveformPreferences");e.player.karaokeRenderer&&(e.player.karaokeRenderer.setWaveformsEnabled(t.enableWaveforms),e.player.karaokeRenderer.setEffectsEnabled(t.enableEffects),e.player.karaokeRenderer.setShowUpcomingLyrics(t.showUpcomingLyrics),e.player.karaokeRenderer.waveformPreferences.overlayOpacity=t.overlayOpacity,e.kaiPlayer?.outputNodes?.PA?.analyser&&e.player.karaokeRenderer.setVisualizationAnalyser(e.kaiPlayer.outputNodes.PA.analyser),t.enableWaveforms&&setTimeout(()=>{e.player.karaokeRenderer.startMicrophoneCapture()},100),setTimeout(()=>e.updateEffectDisplay(),100),await c(e,t))}await new Promise(r=>setTimeout(r,500)),e._pendingMetadata=null}async function g(e,a,n){e.player.karaokeRenderer.effectsCanvas&&e.player.karaokeRenderer.butterchurn&&(e.player.cdgPlayer.setEffectsCanvas(e.player.karaokeRenderer.effectsCanvas,e.player.karaokeRenderer.butterchurn),e.player.cdgPlayer.analyserNode&&e.player.karaokeRenderer.setVisualizationAnalyser(e.player.cdgPlayer.analyserNode),await c(e,n))}async function w(e,a,n){if(e.player.currentFormat="m4a-stems",e.player.currentPlayer=e.kaiPlayer,e.player.currentPlayer.onSongEnded(()=>e.handleSongEnded()),e.kaiPlayer&&e.currentSong){const r={...e.currentSong,audio:e.currentSong.audio?{...e.currentSong.audio,sources:e.currentSong.audio.sources?[...e.currentSong.audio.sources]:[]}:null};await e.kaiPlayer.reinitialize(),await e.kaiPlayer.loadSong(r),!e.currentSong.audio&&r.audio&&(e.currentSong.audio=r.audio),!e.currentSong.lyrics&&r.lyrics&&(e.currentSong.lyrics=r.lyrics)}if(e.player&&e.currentSong){e.player.karaokeRenderer&&e.player.karaokeRenderer.reinitialize();const r={...n,lyrics:e.currentSong.lyrics,duration:e.kaiPlayer?e.kaiPlayer.getDuration():e.currentSong.metadata?.duration||0,audio:e.currentSong.audio,requester:n.requester||a.requester||e.currentSong.requester};e.player.onSongLoaded(r);const t=await window.kaiAPI.settings.get("waveformPreferences");e.player.karaokeRenderer&&(e.player.karaokeRenderer.setWaveformsEnabled(t.enableWaveforms),e.player.karaokeRenderer.setEffectsEnabled(t.enableEffects),e.player.karaokeRenderer.setShowUpcomingLyrics(t.showUpcomingLyrics),e.player.karaokeRenderer.waveformPreferences.overlayOpacity=t.overlayOpacity,e.kaiPlayer?.outputNodes?.PA?.analyser&&e.player.karaokeRenderer.setVisualizationAnalyser(e.kaiPlayer.outputNodes.PA.analyser),t.enableWaveforms&&setTimeout(()=>{e.player.karaokeRenderer.startMicrophoneCapture()},100),setTimeout(()=>e.updateEffectDisplay(),100),await c(e,t))}await new Promise(r=>setTimeout(r,500)),e._pendingMetadata=null}function c(e,a){a.randomEffectOnSong&&e.player.karaokeRenderer.butterchurn&&(e.randomEffectTimeout&&clearTimeout(e.randomEffectTimeout),e.randomEffectTimeout=setTimeout(async()=>{try{await window.kaiAPI.effects.random()}catch(n){console.error("Failed to apply random effect:",n)}},500))}export{P as loadCDGSong,S as loadKAISong,w as loadM4ASong};
2
+ //# sourceMappingURL=songLoaders-CcYVonLu.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"songLoaders-BaTgGib4.js","sources":["../../js/songLoaders.js"],"sourcesContent":["/**\n * Song Loading Utilities\n * Extracted from main.js to simplify song loading logic\n */\n\n/**\n * Load CDG format song\n */\nexport async function loadCDGSong(app, songData, metadata) {\n app.player.currentFormat = 'cdg';\n app.player.currentPlayer = app.player.cdgPlayer;\n\n // Set song end callback\n app.player.currentPlayer.onSongEnded(() => app.handleSongEnded());\n\n // Set up audio context for CDG renderer (PA output only)\n if (!app.kaiPlayer) {\n console.error('💿 Audio engine not initialized');\n app.updateStatus('Error: Audio engine not ready');\n return;\n }\n\n // Get PA audio context for playback\n const paContext = app.kaiPlayer.audioContexts.PA;\n const paMasterGain = app.kaiPlayer.outputNodes.PA.masterGain;\n\n // Create gain node for CDG audio in PA context\n const cdgGainNode = paContext.createGain();\n cdgGainNode.connect(paMasterGain);\n\n // Create analyser in PA context\n const analyserNode = paContext.createAnalyser();\n analyserNode.fftSize = 2048;\n // Analyser taps the signal but doesn't affect routing\n cdgGainNode.connect(analyserNode);\n\n // Set audio context in CDG renderer (PA context for playback)\n app.player.cdgPlayer.setAudioContext(paContext, cdgGainNode, analyserNode);\n\n // Load CDG data\n await app.player.cdgPlayer.loadSong(songData);\n\n // Load microphone settings for CDG (after micEngine is initialized)\n if (app.player.cdgPlayer.micEngine) {\n const micToSpeakers = await window.kaiAPI.settings.get('micToSpeakers', true);\n const enableMic = await window.kaiAPI.settings.get('enableMic', true);\n const autoTunePrefs = await window.kaiAPI.settings.get('autoTunePreferences', {});\n\n // Apply settings to microphone engine\n app.player.cdgPlayer.micEngine.micToSpeakers = micToSpeakers;\n app.player.cdgPlayer.micEngine.enableMic = enableMic;\n\n if (autoTunePrefs.enabled !== undefined) {\n app.player.cdgPlayer.micEngine.autotuneSettings.enabled = autoTunePrefs.enabled;\n }\n if (autoTunePrefs.strength !== undefined) {\n app.player.cdgPlayer.micEngine.autotuneSettings.strength = autoTunePrefs.strength;\n }\n if (autoTunePrefs.speed !== undefined) {\n app.player.cdgPlayer.micEngine.autotuneSettings.speed = autoTunePrefs.speed;\n }\n\n // Start microphone if enabled\n if (enableMic) {\n const devicePrefs = await window.kaiAPI.settings.get('devicePreferences', {});\n const inputDeviceId = devicePrefs?.input?.id || 'default';\n await app.player.cdgPlayer.startMicrophoneInput(inputDeviceId);\n }\n }\n\n // Load and apply waveform preferences from settings for CDG\n const waveformPrefs = await window.kaiAPI.settings.get('waveformPreferences', {\n enableEffects: true,\n randomEffectOnSong: false,\n overlayOpacity: 0.7,\n });\n\n // Load device preferences for microphone\n await app.player.karaokeRenderer.ensureInputDeviceSelection();\n\n // Apply preferences to CDG player\n app.player.cdgPlayer.setOverlayOpacity(waveformPrefs.overlayOpacity);\n app.player.cdgPlayer.setEffectsEnabled(waveformPrefs.enableEffects);\n\n // Set Butterchurn effects canvas for CDG background\n await setupButterchurnForCDG(app, songData, waveformPrefs);\n\n // CDG doesn't use audio engine or lyrics\n // Include requester from songData if not in metadata\n const fullMetadata = {\n ...metadata,\n requester: metadata.requester || songData.requester || app.currentSong?.requester,\n };\n app.player.onSongLoaded(fullMetadata);\n\n // Broadcast that CDG is ready (clear loading state)\n if (window.kaiAPI?.renderer) {\n window.kaiAPI.renderer.songLoaded({\n path: app.currentSong?.originalFilePath || app.currentSong?.filePath,\n metadata: metadata,\n isLoading: false,\n title: metadata?.title || 'CDG Song',\n artist: metadata?.artist || 'Unknown Artist',\n format: 'cdg',\n duration: app.player.cdgPlayer?.getDuration() || 0,\n requester: app.currentSong?.requester || 'KJ',\n });\n }\n\n // Controls now managed by React TransportControlsWrapper\n app.updateStatus(`Loaded: ${metadata?.title || 'CDG Song'}`);\n}\n\n/**\n * Load KAI format song\n */\nexport async function loadKAISong(app, songData, metadata) {\n app.player.currentFormat = 'kai';\n app.player.currentPlayer = app.kaiPlayer;\n\n // Set song end callback\n app.player.currentPlayer.onSongEnded(() => app.handleSongEnded());\n\n // CLEAN SLATE APPROACH: Reinitialize audio engine\n if (app.kaiPlayer && app.currentSong) {\n // Create a backup copy of the song data BEFORE reinitialize\n const songDataBackup = {\n ...app.currentSong,\n audio: app.currentSong.audio\n ? {\n ...app.currentSong.audio,\n sources: app.currentSong.audio.sources ? [...app.currentSong.audio.sources] : [],\n }\n : null,\n };\n\n await app.kaiPlayer.reinitialize();\n await app.kaiPlayer.loadSong(songDataBackup);\n\n // Restore the original song data if it was corrupted\n if (!app.currentSong.audio && songDataBackup.audio) {\n app.currentSong.audio = songDataBackup.audio;\n }\n if (!app.currentSong.lyrics && songDataBackup.lyrics) {\n app.currentSong.lyrics = songDataBackup.lyrics;\n }\n }\n\n // CLEAN SLATE APPROACH: Reinitialize karaoke renderer\n if (app.player && app.currentSong) {\n if (app.player.karaokeRenderer) {\n app.player.karaokeRenderer.reinitialize();\n }\n\n // Pass full song data which includes lyrics, audio sources, and updated duration from audio engine\n const fullMetadata = {\n ...metadata,\n lyrics: app.currentSong.lyrics,\n duration: app.kaiPlayer\n ? app.kaiPlayer.getDuration()\n : app.currentSong.metadata?.duration || 0,\n audio: app.currentSong.audio, // Include audio sources for vocals waveform\n requester: metadata.requester || songData.requester || app.currentSong.requester,\n };\n app.player.onSongLoaded(fullMetadata);\n\n // Load and apply waveform preferences from settings for KAI\n // Defaults are now provided by settingsManager from shared/defaults.js\n const waveformPrefs = await window.kaiAPI.settings.get('waveformPreferences');\n\n // Apply preferences to karaokeRenderer\n if (app.player.karaokeRenderer) {\n app.player.karaokeRenderer.setWaveformsEnabled(waveformPrefs.enableWaveforms);\n app.player.karaokeRenderer.setEffectsEnabled(waveformPrefs.enableEffects);\n app.player.karaokeRenderer.setShowUpcomingLyrics(waveformPrefs.showUpcomingLyrics);\n app.player.karaokeRenderer.waveformPreferences.overlayOpacity = waveformPrefs.overlayOpacity;\n\n // Connect butterchurn to PA analyser for visualization (KAI format)\n if (app.kaiPlayer?.outputNodes?.PA?.analyser) {\n app.player.karaokeRenderer.setVisualizationAnalyser(app.kaiPlayer.outputNodes.PA.analyser);\n }\n\n // Restart microphone capture if waveforms are enabled\n if (waveformPrefs.enableWaveforms) {\n setTimeout(() => {\n app.player.karaokeRenderer.startMicrophoneCapture();\n }, 100);\n }\n\n // Update effect display with current preset\n setTimeout(() => app.updateEffectDisplay(), 100);\n\n // Apply random effect if enabled\n await applyRandomEffectIfEnabled(app, waveformPrefs);\n }\n }\n\n // Wait for all contexts and buffers to be ready\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n // Clear pending metadata\n app._pendingMetadata = null;\n}\n\n/**\n * Setup Butterchurn for CDG visualization\n */\nasync function setupButterchurnForCDG(app, songData, waveformPrefs) {\n if (app.player.karaokeRenderer.effectsCanvas && app.player.karaokeRenderer.butterchurn) {\n app.player.cdgPlayer.setEffectsCanvas(\n app.player.karaokeRenderer.effectsCanvas,\n app.player.karaokeRenderer.butterchurn\n );\n\n // Connect butterchurn to CDG's PA analyser (already connected to CDG audio source)\n if (app.player.cdgPlayer.analyserNode) {\n app.player.karaokeRenderer.setVisualizationAnalyser(app.player.cdgPlayer.analyserNode);\n }\n\n // Apply random effect if enabled\n await applyRandomEffectIfEnabled(app, waveformPrefs);\n }\n}\n\n/**\n * Load M4A Stems format song\n */\nexport async function loadM4ASong(app, songData, metadata) {\n app.player.currentFormat = 'm4a-stems';\n app.player.currentPlayer = app.kaiPlayer;\n\n // Set song end callback\n app.player.currentPlayer.onSongEnded(() => app.handleSongEnded());\n\n // CLEAN SLATE APPROACH: Reinitialize audio engine (same as KAI)\n if (app.kaiPlayer && app.currentSong) {\n // Create a backup copy of the song data BEFORE reinitialize\n const songDataBackup = {\n ...app.currentSong,\n audio: app.currentSong.audio\n ? {\n ...app.currentSong.audio,\n sources: app.currentSong.audio.sources ? [...app.currentSong.audio.sources] : [],\n }\n : null,\n };\n\n await app.kaiPlayer.reinitialize();\n await app.kaiPlayer.loadSong(songDataBackup);\n\n // Restore the original song data if it was corrupted\n if (!app.currentSong.audio && songDataBackup.audio) {\n app.currentSong.audio = songDataBackup.audio;\n }\n if (!app.currentSong.lyrics && songDataBackup.lyrics) {\n app.currentSong.lyrics = songDataBackup.lyrics;\n }\n }\n\n // CLEAN SLATE APPROACH: Reinitialize karaoke renderer\n if (app.player && app.currentSong) {\n if (app.player.karaokeRenderer) {\n app.player.karaokeRenderer.reinitialize();\n }\n\n // Pass full song data which includes lyrics, audio sources, and updated duration\n const fullMetadata = {\n ...metadata,\n lyrics: app.currentSong.lyrics,\n duration: app.kaiPlayer\n ? app.kaiPlayer.getDuration()\n : app.currentSong.metadata?.duration || 0,\n audio: app.currentSong.audio, // Include audio sources for vocals waveform\n requester: metadata.requester || songData.requester || app.currentSong.requester,\n };\n app.player.onSongLoaded(fullMetadata);\n\n // Load and apply waveform preferences from settings\n // Defaults are now provided by settingsManager from shared/defaults.js\n const waveformPrefs = await window.kaiAPI.settings.get('waveformPreferences');\n\n // Apply preferences to karaokeRenderer\n if (app.player.karaokeRenderer) {\n app.player.karaokeRenderer.setWaveformsEnabled(waveformPrefs.enableWaveforms);\n app.player.karaokeRenderer.setEffectsEnabled(waveformPrefs.enableEffects);\n app.player.karaokeRenderer.setShowUpcomingLyrics(waveformPrefs.showUpcomingLyrics);\n app.player.karaokeRenderer.waveformPreferences.overlayOpacity = waveformPrefs.overlayOpacity;\n\n // Connect butterchurn to PA analyser for visualization (M4A format)\n if (app.kaiPlayer?.outputNodes?.PA?.analyser) {\n app.player.karaokeRenderer.setVisualizationAnalyser(app.kaiPlayer.outputNodes.PA.analyser);\n }\n\n // Restart microphone capture if waveforms are enabled\n if (waveformPrefs.enableWaveforms) {\n setTimeout(() => {\n app.player.karaokeRenderer.startMicrophoneCapture();\n }, 100);\n }\n\n // Update effect display with current preset\n setTimeout(() => app.updateEffectDisplay(), 100);\n\n // Apply random effect if enabled\n await applyRandomEffectIfEnabled(app, waveformPrefs);\n }\n }\n\n // Wait for all contexts and buffers to be ready\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n // Clear pending metadata\n app._pendingMetadata = null;\n}\n\n/**\n * Apply random Butterchurn effect if enabled (with debouncing)\n */\nfunction applyRandomEffectIfEnabled(app, waveformPrefs) {\n if (waveformPrefs.randomEffectOnSong && app.player.karaokeRenderer.butterchurn) {\n // Clear any existing timeout\n if (app.randomEffectTimeout) {\n clearTimeout(app.randomEffectTimeout);\n }\n\n app.randomEffectTimeout = setTimeout(async () => {\n try {\n await window.kaiAPI.effects.random();\n } catch (error) {\n console.error('Failed to apply random effect:', error);\n }\n }, 500);\n }\n}\n"],"names":["loadCDGSong","app","songData","metadata","paContext","paMasterGain","cdgGainNode","analyserNode","micToSpeakers","enableMic","autoTunePrefs","inputDeviceId","waveformPrefs","setupButterchurnForCDG","fullMetadata","loadKAISong","songDataBackup","applyRandomEffectIfEnabled","resolve","error"],"mappings":"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"}
1
+ {"version":3,"file":"songLoaders-CcYVonLu.js","sources":["../../js/songLoaders.js"],"sourcesContent":["/**\n * Song Loading Utilities\n * Extracted from main.js to simplify song loading logic\n */\n\n/**\n * Load CDG format song\n */\nexport async function loadCDGSong(app, songData, metadata) {\n app.player.currentFormat = 'cdg';\n app.player.currentPlayer = app.player.cdgPlayer;\n\n // Set song end callback\n app.player.currentPlayer.onSongEnded(() => app.handleSongEnded());\n\n // Set up audio context for CDG renderer (PA output only)\n if (!app.kaiPlayer) {\n console.error('💿 Audio engine not initialized');\n app.updateStatus('Error: Audio engine not ready');\n return;\n }\n\n // Get PA audio context for playback\n const paContext = app.kaiPlayer.audioContexts.PA;\n const paMasterGain = app.kaiPlayer.outputNodes.PA.masterGain;\n\n // Create gain node for CDG audio in PA context\n const cdgGainNode = paContext.createGain();\n cdgGainNode.connect(paMasterGain);\n\n // Create analyser in PA context\n const analyserNode = paContext.createAnalyser();\n analyserNode.fftSize = 2048;\n // Analyser taps the signal but doesn't affect routing\n cdgGainNode.connect(analyserNode);\n\n // Set audio context in CDG renderer (PA context for playback)\n app.player.cdgPlayer.setAudioContext(paContext, cdgGainNode, analyserNode);\n\n // Load CDG data\n await app.player.cdgPlayer.loadSong(songData);\n\n // Load microphone settings for CDG (after micEngine is initialized)\n if (app.player.cdgPlayer.micEngine) {\n const micToSpeakers = await window.kaiAPI.settings.get('micToSpeakers', true);\n const enableMic = await window.kaiAPI.settings.get('enableMic', true);\n const autoTunePrefs = await window.kaiAPI.settings.get('autoTunePreferences', {});\n\n // Apply settings to microphone engine\n app.player.cdgPlayer.micEngine.micToSpeakers = micToSpeakers;\n app.player.cdgPlayer.micEngine.enableMic = enableMic;\n\n if (autoTunePrefs.enabled !== undefined) {\n app.player.cdgPlayer.micEngine.autotuneSettings.enabled = autoTunePrefs.enabled;\n }\n if (autoTunePrefs.strength !== undefined) {\n app.player.cdgPlayer.micEngine.autotuneSettings.strength = autoTunePrefs.strength;\n }\n if (autoTunePrefs.speed !== undefined) {\n app.player.cdgPlayer.micEngine.autotuneSettings.speed = autoTunePrefs.speed;\n }\n\n // Start microphone if enabled\n if (enableMic) {\n const devicePrefs = await window.kaiAPI.settings.get('devicePreferences', {});\n const inputDeviceId = devicePrefs?.input?.id || 'default';\n await app.player.cdgPlayer.startMicrophoneInput(inputDeviceId);\n }\n }\n\n // Load and apply waveform preferences from settings for CDG\n const waveformPrefs = await window.kaiAPI.settings.get('waveformPreferences', {\n enableEffects: true,\n randomEffectOnSong: false,\n overlayOpacity: 0.7,\n });\n\n // Load device preferences for microphone\n await app.player.karaokeRenderer.ensureInputDeviceSelection();\n\n // Apply preferences to CDG player\n app.player.cdgPlayer.setOverlayOpacity(waveformPrefs.overlayOpacity);\n app.player.cdgPlayer.setEffectsEnabled(waveformPrefs.enableEffects);\n\n // Set Butterchurn effects canvas for CDG background\n await setupButterchurnForCDG(app, songData, waveformPrefs);\n\n // CDG doesn't use audio engine or lyrics\n // Include requester from songData if not in metadata\n const fullMetadata = {\n ...metadata,\n requester: metadata.requester || songData.requester || app.currentSong?.requester,\n };\n app.player.onSongLoaded(fullMetadata);\n\n // Broadcast that CDG is ready (clear loading state)\n if (window.kaiAPI?.renderer) {\n window.kaiAPI.renderer.songLoaded({\n path: app.currentSong?.originalFilePath || app.currentSong?.filePath,\n metadata: metadata,\n isLoading: false,\n title: metadata?.title || 'CDG Song',\n artist: metadata?.artist || 'Unknown Artist',\n format: 'cdg',\n duration: app.player.cdgPlayer?.getDuration() || 0,\n requester: app.currentSong?.requester || 'KJ',\n });\n }\n\n // Controls now managed by React TransportControlsWrapper\n app.updateStatus(`Loaded: ${metadata?.title || 'CDG Song'}`);\n}\n\n/**\n * Load KAI format song\n */\nexport async function loadKAISong(app, songData, metadata) {\n app.player.currentFormat = 'kai';\n app.player.currentPlayer = app.kaiPlayer;\n\n // Set song end callback\n app.player.currentPlayer.onSongEnded(() => app.handleSongEnded());\n\n // CLEAN SLATE APPROACH: Reinitialize audio engine\n if (app.kaiPlayer && app.currentSong) {\n // Create a backup copy of the song data BEFORE reinitialize\n const songDataBackup = {\n ...app.currentSong,\n audio: app.currentSong.audio\n ? {\n ...app.currentSong.audio,\n sources: app.currentSong.audio.sources ? [...app.currentSong.audio.sources] : [],\n }\n : null,\n };\n\n await app.kaiPlayer.reinitialize();\n await app.kaiPlayer.loadSong(songDataBackup);\n\n // Restore the original song data if it was corrupted\n if (!app.currentSong.audio && songDataBackup.audio) {\n app.currentSong.audio = songDataBackup.audio;\n }\n if (!app.currentSong.lyrics && songDataBackup.lyrics) {\n app.currentSong.lyrics = songDataBackup.lyrics;\n }\n }\n\n // CLEAN SLATE APPROACH: Reinitialize karaoke renderer\n if (app.player && app.currentSong) {\n if (app.player.karaokeRenderer) {\n app.player.karaokeRenderer.reinitialize();\n }\n\n // Pass full song data which includes lyrics, audio sources, and updated duration from audio engine\n const fullMetadata = {\n ...metadata,\n lyrics: app.currentSong.lyrics,\n duration: app.kaiPlayer\n ? app.kaiPlayer.getDuration()\n : app.currentSong.metadata?.duration || 0,\n audio: app.currentSong.audio, // Include audio sources for vocals waveform\n requester: metadata.requester || songData.requester || app.currentSong.requester,\n };\n app.player.onSongLoaded(fullMetadata);\n\n // Load and apply waveform preferences from settings for KAI\n // Defaults are now provided by settingsManager from shared/defaults.js\n const waveformPrefs = await window.kaiAPI.settings.get('waveformPreferences');\n\n // Apply preferences to karaokeRenderer\n if (app.player.karaokeRenderer) {\n app.player.karaokeRenderer.setWaveformsEnabled(waveformPrefs.enableWaveforms);\n app.player.karaokeRenderer.setEffectsEnabled(waveformPrefs.enableEffects);\n app.player.karaokeRenderer.setShowUpcomingLyrics(waveformPrefs.showUpcomingLyrics);\n app.player.karaokeRenderer.waveformPreferences.overlayOpacity = waveformPrefs.overlayOpacity;\n\n // Connect butterchurn to PA analyser for visualization (KAI format)\n if (app.kaiPlayer?.outputNodes?.PA?.analyser) {\n app.player.karaokeRenderer.setVisualizationAnalyser(app.kaiPlayer.outputNodes.PA.analyser);\n }\n\n // Restart microphone capture if waveforms are enabled\n if (waveformPrefs.enableWaveforms) {\n setTimeout(() => {\n app.player.karaokeRenderer.startMicrophoneCapture();\n }, 100);\n }\n\n // Update effect display with current preset\n setTimeout(() => app.updateEffectDisplay(), 100);\n\n // Apply random effect if enabled\n await applyRandomEffectIfEnabled(app, waveformPrefs);\n }\n }\n\n // Wait for all contexts and buffers to be ready\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n // Clear pending metadata\n app._pendingMetadata = null;\n}\n\n/**\n * Setup Butterchurn for CDG visualization\n */\nasync function setupButterchurnForCDG(app, songData, waveformPrefs) {\n if (app.player.karaokeRenderer.effectsCanvas && app.player.karaokeRenderer.butterchurn) {\n app.player.cdgPlayer.setEffectsCanvas(\n app.player.karaokeRenderer.effectsCanvas,\n app.player.karaokeRenderer.butterchurn\n );\n\n // Connect butterchurn to CDG's PA analyser (already connected to CDG audio source)\n if (app.player.cdgPlayer.analyserNode) {\n app.player.karaokeRenderer.setVisualizationAnalyser(app.player.cdgPlayer.analyserNode);\n }\n\n // Apply random effect if enabled\n await applyRandomEffectIfEnabled(app, waveformPrefs);\n }\n}\n\n/**\n * Load M4A Stems format song\n */\nexport async function loadM4ASong(app, songData, metadata) {\n app.player.currentFormat = 'm4a-stems';\n app.player.currentPlayer = app.kaiPlayer;\n\n // Set song end callback\n app.player.currentPlayer.onSongEnded(() => app.handleSongEnded());\n\n // CLEAN SLATE APPROACH: Reinitialize audio engine (same as KAI)\n if (app.kaiPlayer && app.currentSong) {\n // Create a backup copy of the song data BEFORE reinitialize\n const songDataBackup = {\n ...app.currentSong,\n audio: app.currentSong.audio\n ? {\n ...app.currentSong.audio,\n sources: app.currentSong.audio.sources ? [...app.currentSong.audio.sources] : [],\n }\n : null,\n };\n\n await app.kaiPlayer.reinitialize();\n await app.kaiPlayer.loadSong(songDataBackup);\n\n // Restore the original song data if it was corrupted\n if (!app.currentSong.audio && songDataBackup.audio) {\n app.currentSong.audio = songDataBackup.audio;\n }\n if (!app.currentSong.lyrics && songDataBackup.lyrics) {\n app.currentSong.lyrics = songDataBackup.lyrics;\n }\n }\n\n // CLEAN SLATE APPROACH: Reinitialize karaoke renderer\n if (app.player && app.currentSong) {\n if (app.player.karaokeRenderer) {\n app.player.karaokeRenderer.reinitialize();\n }\n\n // Pass full song data which includes lyrics, audio sources, and updated duration\n const fullMetadata = {\n ...metadata,\n lyrics: app.currentSong.lyrics,\n duration: app.kaiPlayer\n ? app.kaiPlayer.getDuration()\n : app.currentSong.metadata?.duration || 0,\n audio: app.currentSong.audio, // Include audio sources for vocals waveform\n requester: metadata.requester || songData.requester || app.currentSong.requester,\n };\n app.player.onSongLoaded(fullMetadata);\n\n // Load and apply waveform preferences from settings\n // Defaults are now provided by settingsManager from shared/defaults.js\n const waveformPrefs = await window.kaiAPI.settings.get('waveformPreferences');\n\n // Apply preferences to karaokeRenderer\n if (app.player.karaokeRenderer) {\n app.player.karaokeRenderer.setWaveformsEnabled(waveformPrefs.enableWaveforms);\n app.player.karaokeRenderer.setEffectsEnabled(waveformPrefs.enableEffects);\n app.player.karaokeRenderer.setShowUpcomingLyrics(waveformPrefs.showUpcomingLyrics);\n app.player.karaokeRenderer.waveformPreferences.overlayOpacity = waveformPrefs.overlayOpacity;\n\n // Connect butterchurn to PA analyser for visualization (M4A format)\n if (app.kaiPlayer?.outputNodes?.PA?.analyser) {\n app.player.karaokeRenderer.setVisualizationAnalyser(app.kaiPlayer.outputNodes.PA.analyser);\n }\n\n // Restart microphone capture if waveforms are enabled\n if (waveformPrefs.enableWaveforms) {\n setTimeout(() => {\n app.player.karaokeRenderer.startMicrophoneCapture();\n }, 100);\n }\n\n // Update effect display with current preset\n setTimeout(() => app.updateEffectDisplay(), 100);\n\n // Apply random effect if enabled\n await applyRandomEffectIfEnabled(app, waveformPrefs);\n }\n }\n\n // Wait for all contexts and buffers to be ready\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n // Clear pending metadata\n app._pendingMetadata = null;\n}\n\n/**\n * Apply random Butterchurn effect if enabled (with debouncing)\n */\nfunction applyRandomEffectIfEnabled(app, waveformPrefs) {\n if (waveformPrefs.randomEffectOnSong && app.player.karaokeRenderer.butterchurn) {\n // Clear any existing timeout\n if (app.randomEffectTimeout) {\n clearTimeout(app.randomEffectTimeout);\n }\n\n app.randomEffectTimeout = setTimeout(async () => {\n try {\n await window.kaiAPI.effects.random();\n } catch (error) {\n console.error('Failed to apply random effect:', error);\n }\n }, 500);\n }\n}\n"],"names":["loadCDGSong","app","songData","metadata","paContext","paMasterGain","cdgGainNode","analyserNode","micToSpeakers","enableMic","autoTunePrefs","inputDeviceId","waveformPrefs","setupButterchurnForCDG","fullMetadata","loadKAISong","songDataBackup","applyRandomEffectIfEnabled","resolve","loadM4ASong","error"],"mappings":"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"}