training-academy/src/features/audio/audioManager.js

1068 lines
42 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Audio Manager - Centralized Audio System for Gooner Training Academy
* Handles multiple audio categories with volume controls and fade effects
*/
class AudioManager {
constructor(dataManager, game = null) {
this.dataManager = dataManager;
this.game = game; // Reference to game instance for state checking
this.audioEnabled = true; // Global flag to disable all audio
this.isPlaylistMode = false; // New playlist mode flag
this.currentPlaylistAudio = null; // Current playlist audio element
this.playlistQueue = []; // Queue of audio files to play
this.currentPlaylistIndex = 0; // Current position in playlist
this.audioContext = null;
this.masterGain = null;
// Audio category configurations - simplified to just background audio
this.categories = {
background: {
volume: 0.7,
enabled: true,
currentAudio: null,
fadeTimeout: null
}
};
this.masterVolume = 0.7;
this.isInitialized = false;
this.audioLibrary = {};
this.init();
}
async init() {
console.log('AudioManager initializing...');
// Check if we're in a problematic browser environment
const isElectron = window.electronAPI !== undefined;
const isFileProtocol = window.location.protocol === 'file:';
if (!isElectron && isFileProtocol) {
console.warn('⚠️ Browser file:// mode detected - audio may not work due to CORS restrictions');
console.warn('💡 For full audio support, run: npm start (Electron desktop app)');
// Option to disable audio in browser mode
const disableAudio = localStorage.getItem('disableBrowserAudio') === 'true';
if (disableAudio) {
console.log('🔇 Audio disabled for browser mode');
this.isInitialized = true;
return;
}
}
// Load saved settings
this.loadSettings();
// Initialize Web Audio API for better control
try {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.audioContext.createGain();
this.masterGain.connect(this.audioContext.destination);
this.masterGain.gain.value = this.masterVolume;
} catch (error) {
console.warn('Web Audio API not available, falling back to HTML5 audio:', error);
}
// Discover available audio files
await this.discoverAudioLibrary();
this.isInitialized = true;
console.log('AudioManager initialized successfully');
console.log('Available audio categories:', Object.keys(this.audioLibrary));
}
// Clean up corrupted audio data (paths with URL encoding issues)
async cleanupCorruptedAudioData() {
if (!window.game || !window.game.dataManager) {
return;
}
const customAudio = window.game.dataManager.get('customAudio') || { background: [], ambient: [] };
let hasCorruption = false;
// Check for and fix corrupted paths
['background', 'ambient'].forEach(category => {
if (customAudio[category]) {
customAudio[category] = customAudio[category].filter(audioFile => {
// Remove files with corrupted paths (containing URL encoding artifacts)
if (audioFile.path && audioFile.path.includes('%08')) {
console.log(`Removing corrupted audio file: ${audioFile.path}`);
hasCorruption = true;
return false;
}
return true;
});
}
});
// Remove any references to old 'effects' category
if (customAudio.effects) {
console.log('Removing deprecated effects category');
delete customAudio.effects;
hasCorruption = true;
}
if (hasCorruption) {
console.log('🧹 Cleaned up corrupted audio data');
window.game.dataManager.set('customAudio', customAudio);
}
}
async discoverAudioLibrary() {
console.log('🎵 Discovering audio library from user-managed files...');
// Clean up any corrupted audio data first
await this.cleanupCorruptedAudioData();
// Instead of hardcoded files, use the dynamic audio management system
// This integrates with the existing "Manage Audio" menu that users can control
// Initialize the library structure
this.audioLibrary = {
background: {
general: [] // Single category for all background audio
}
};
// Get audio files from the existing audio management system
if (window.game && window.game.dataManager) {
const customAudio = window.game.dataManager.get('customAudio') || { background: [], ambient: [] };
// Combine all audio categories into a single "background" category
const allAudioFiles = [
...(customAudio.background || []),
...(customAudio.ambient || [])
];
// Add user's audio files to the library (only if they actually exist)
const validFiles = [];
for (const audioFile of allAudioFiles) {
if (audioFile.enabled !== false) { // Only include enabled files
// Debug path corruption issue
console.log(`Processing audio file:`, audioFile);
console.log(`Path before adding:`, audioFile.path);
// Fix path corruption - decode any URL encoding and normalize path
let cleanPath = audioFile.path;
if (cleanPath.includes('%')) {
try {
cleanPath = decodeURIComponent(cleanPath);
console.log(`Decoded path: ${cleanPath}`);
} catch (e) {
console.warn(`Failed to decode path: ${cleanPath}`, e);
}
}
// For Electron with absolute Windows paths, keep them as-is
// Don't convert backslashes to forward slashes for Windows absolute paths
if (window.electronAPI && cleanPath.match(/^[A-Za-z]:\\/)) {
// Windows absolute path - convert backslashes to forward slashes for web compatibility
cleanPath = cleanPath.replace(/\\/g, '/');
console.log(`Normalized Windows path: ${cleanPath}`);
}
// For Electron, validate file exists before adding
if (window.electronAPI && window.electronAPI.fs) {
try {
const exists = await window.electronAPI.fs.existsSync(cleanPath.replace(/\//g, '\\'));
if (!exists) {
console.warn(`⚠️ File does not exist, skipping: ${cleanPath}`);
continue;
}
} catch (error) {
console.warn(`⚠️ Could not verify file existence: ${cleanPath}`, error);
continue;
}
}
const audioFileObj = {
name: audioFile.name || 'Unnamed Audio',
path: cleanPath,
element: null
};
this.audioLibrary.background.general.push(audioFileObj);
validFiles.push(audioFileObj);
console.log(`Added user audio file: ${cleanPath}`);
}
}
// Update storage to remove non-existent files
if (validFiles.length !== allAudioFiles.length && window.game && window.game.dataManager) {
console.log(`🧹 Removing ${allAudioFiles.length - validFiles.length} non-existent files from storage`);
const updatedCustomAudio = {
background: validFiles.filter(f => f.path.includes('/background/')),
ambient: validFiles.filter(f => f.path.includes('/ambient/'))
};
window.game.dataManager.set('customAudio', updatedCustomAudio);
}
console.log(`🎵 Loaded ${validFiles.length} user audio files`);
}
// If no user audio is available, create empty structure
if (this.audioLibrary.background.general.length === 0) {
console.log('🎵 No user audio files found.');
console.log('🎵 To add background audio:');
console.log(' 1. Go to the main menu');
console.log(' 2. Click "Manage Audio"');
console.log(' 3. Import audio files using the "Import" buttons');
console.log(' 4. Audio will automatically play during tasks');
}
console.log('🎵 Audio library discovery completed:', this.audioLibrary);
}
loadSettings() {
const savedSettings = this.dataManager.get('audioSettings') || {};
// Load master volume
this.masterVolume = savedSettings.masterVolume || 0.7;
// Load category settings
for (const category in this.categories) {
if (savedSettings.categories && savedSettings.categories[category]) {
this.categories[category] = {
...this.categories[category],
...savedSettings.categories[category]
};
}
}
}
saveSettings() {
const settings = {
masterVolume: this.masterVolume,
categories: this.categories
};
this.dataManager.set('audioSettings', settings);
}
// Master volume control
setMasterVolume(volume) {
this.masterVolume = Math.max(0, Math.min(1, volume));
if (this.masterGain) {
this.masterGain.gain.value = this.masterVolume;
}
this.saveSettings();
}
getMasterVolume() {
return this.masterVolume;
}
// Category volume control
setCategoryVolume(category, volume) {
if (this.categories[category]) {
this.categories[category].volume = Math.max(0, Math.min(1, volume));
// Update currently playing audio if any
if (this.categories[category].currentAudio) {
this.categories[category].currentAudio.volume =
this.categories[category].volume * this.masterVolume;
}
this.saveSettings();
}
}
getCategoryVolume(category) {
return this.categories[category]?.volume || 0;
}
// Enable/disable categories
setCategoryEnabled(category, enabled) {
if (this.categories[category]) {
this.categories[category].enabled = enabled;
// Stop currently playing audio if disabled
if (!enabled && this.categories[category].currentAudio) {
this.stopCategory(category);
}
this.saveSettings();
}
}
isCategoryEnabled(category) {
return this.categories[category]?.enabled || false;
}
// Play audio from library
async playAudio(category, subcategory, fileName = null, options = {}) {
console.log(`PlayAudio called: ${category}/${subcategory}/${fileName || 'random'}`);
// Check if audio is globally disabled
if (!this.audioEnabled) {
console.log('Audio globally disabled - blocking playback');
return null;
}
// Check if we're in a skip audio state (for consequences)
if (this.game && this.game.gameState && this.game.gameState.skipAudioPlayed) {
console.log('Skip audio played flag set - blocking background audio during consequence transition');
return null;
}
if (!this.isInitialized) {
console.warn('AudioManager not initialized yet');
return null;
}
// Check if we're in browser mode and warn about limitations
const isElectron = window.electronAPI !== undefined;
if (!isElectron) {
console.warn('Running in browser mode - audio may have limitations due to file:// restrictions');
}
// Check if the game is still running - prevent audio after game ends
if (this.game && this.game.gameState && !this.game.gameState.isRunning) {
console.log(`Game is not running - blocking audio playback for ${category}/${subcategory}`);
return null;
}
if (!this.isCategoryEnabled(category)) {
console.log(`Audio category ${category} is disabled`);
return null;
}
const categoryLib = this.audioLibrary[category];
if (!categoryLib || !categoryLib[subcategory]) {
console.warn(`Audio not found: ${category}/${subcategory}. Available:`, Object.keys(this.audioLibrary));
console.warn(`Category structure:`, categoryLib);
return null;
}
const audioFiles = categoryLib[subcategory];
if (audioFiles.length === 0) {
console.warn(`No audio files available in ${category}/${subcategory}`);
return null;
}
// Select audio file
let audioFile;
if (fileName) {
audioFile = audioFiles.find(f => f.name === fileName);
if (!audioFile) {
console.warn(`Specific audio file not found: ${fileName}. Available:`, audioFiles.map(f => f.name));
return null;
}
} else {
// Random selection
audioFile = audioFiles[Math.floor(Math.random() * audioFiles.length)];
}
console.log(`Selected audio file: ${audioFile.path}`);
// Validate the audio file path
if (!audioFile.path || audioFile.path.trim() === '') {
console.error(`Invalid audio file path: ${audioFile.path}`);
return null;
}
// Stop any currently playing audio in this category immediately and synchronously
if (this.categories[category].currentAudio) {
const existingAudio = this.categories[category].currentAudio;
existingAudio.pause();
existingAudio.currentTime = 0;
existingAudio._stopped = true; // Mark as stopped to prevent further playback
console.log(`[src debug] Stopping previous audio:`, existingAudio.src);
// Don't clear src as it causes corruption - just pause and reset time
// Also remove from DOM if it exists
if (existingAudio.parentNode) {
existingAudio.parentNode.removeChild(existingAudio);
}
this.categories[category].currentAudio = null;
console.log(`Forcibly stopped existing audio in category: ${category}`);
}
// Clear any pending fade timeout for this category
if (this.categories[category].fadeTimeout) {
clearTimeout(this.categories[category].fadeTimeout);
this.categories[category].fadeTimeout = null;
}
// Create and configure audio element
const audio = new Audio();
audio._stopped = false; // Flag to prevent orphaned playback
// Set the source with proper error handling
try {
console.log(`Setting audio source to: ${audioFile.path}`);
console.log(`Audio file object:`, audioFile);
// Debug: Check if path is already corrupted
if (audioFile.path.includes('%')) {
console.error(`Path already contains URL encoding: ${audioFile.path}`);
}
// For Electron, construct proper file:// URL for absolute paths
let audioSrc = audioFile.path;
if (window.electronAPI && audioFile.path.match(/^[A-Za-z]:\//)) {
// Absolute Windows path - convert to file:// URL
audioSrc = `file:///${audioFile.path}`;
console.log(`Converted to file URL: ${audioSrc}`);
} else if (window.electronAPI && !audioFile.path.startsWith('file://')) {
// Relative path in Electron - make it relative to app root
audioSrc = audioFile.path;
console.log(`Using relative path: ${audioSrc}`);
}
console.log(`[src debug] Setting audio.src to:`, audioSrc);
audio.src = audioSrc;
console.log(`[src debug] audio.src after assignment:`, audio.src);
} catch (error) {
console.error(`Error setting audio source: ${audioFile.path}`, error);
return null;
}
audio.volume = this.categories[category].volume * this.masterVolume;
audio.loop = options.loop || false;
console.log(`Audio volume set to: ${audio.volume} (category: ${this.categories[category].volume}, master: ${this.masterVolume})`);
// Add comprehensive error handling with retry logic
audio.addEventListener('error', (e) => {
console.error(`Audio file failed to load: ${audioFile.path}`, e);
console.error('Error details:', {
error: e.target.error,
networkState: e.target.networkState,
readyState: e.target.readyState,
src: e.target.src,
srcAttribute: audio.getAttribute('src'),
srcProperty: audio.src,
errorCode: e.target.error?.code,
errorMessage: e.target.error?.message
});
// Clean up failed audio element
if (this.categories[category].currentAudio === audio) {
this.categories[category].currentAudio = null;
}
// If this was a specific file request and it failed, don't retry
if (fileName) {
console.log(`Specific audio file ${fileName} failed, not retrying`);
return;
}
// For browser mode with file:// restrictions, disable retries to prevent spam
const isElectron = window.electronAPI !== undefined;
if (!isElectron && window.location.protocol === 'file:') {
console.warn('Browser file:// mode detected - disabling audio retries due to CORS restrictions');
return;
}
// For random selection, try a different file if available
const remainingFiles = audioFiles.filter(f => f !== audioFile);
if (remainingFiles.length > 0) {
console.log(`Retrying with different audio file from ${category}/${subcategory}`);
// Try again with a different random file after a short delay
setTimeout(() => {
// Check if game is still running before retrying
if (!window.game || !window.game.running) {
console.log('Game not running - canceling audio retry');
return;
}
this.playAudio(category, subcategory, null, options);
}, 500);
}
});
audio.addEventListener('loadstart', () => {
console.log(`Started loading: ${audioFile.path}`);
});
audio.addEventListener('canplay', () => {
if (audio._stopped) {
console.log(`Audio ready but stopped - not playing: ${audioFile.path}`);
return;
}
console.log(`Audio ready to play: ${audioFile.path}`);
});
audio.addEventListener('loadeddata', () => {
console.log(`Audio data loaded: ${audioFile.path}`);
});
// Set up event handlers
audio.addEventListener('ended', () => {
if (this.categories[category].currentAudio === audio) {
this.categories[category].currentAudio = null;
}
});
// Store reference
this.categories[category].currentAudio = audio;
// Use a small delay before attempting to play to ensure audio element is ready
try {
// First, load the audio
audio.load();
// Small delay to ensure the audio element is properly initialized
await new Promise(resolve => setTimeout(resolve, 50));
// Check if audio was stopped while loading
if (audio._stopped) {
console.log(`Audio was stopped while loading - not playing: ${audioFile.path}`);
return null;
}
await audio.play();
console.log(`Playing audio: ${category}/${subcategory}/${audioFile.name}`);
// Handle fade in
if (options.fadeIn) {
this.fadeIn(audio, options.fadeIn);
}
return audio;
} catch (error) {
console.error(`Failed to play audio: ${audioFile.path}`, error);
console.error(`Play error details:`, {
name: error.name,
message: error.message,
audioSrc: audio.src,
audioReadyState: audio.readyState,
audioNetworkState: audio.networkState
});
this.categories[category].currentAudio = null;
return null;
}
}
// Stop audio in specific category
stopCategory(category, fadeOut = 0) {
if (!this.categories[category] || !this.categories[category].currentAudio) {
return;
}
const audio = this.categories[category].currentAudio;
console.log(`Stopping audio in category: ${category} with fadeOut: ${fadeOut}ms`);
if (fadeOut > 0) {
this.fadeOut(audio, fadeOut, () => {
audio.pause();
audio.currentTime = 0; // Reset to beginning
console.log(`[src debug] Clearing src of stopped audio:`, audio.src);
audio.src = '';
console.log(`[src debug] src after clearing:`, audio.src);
// Also remove from DOM if it exists
if (audio.parentNode) {
audio.parentNode.removeChild(audio);
}
this.categories[category].currentAudio = null;
console.log(`Audio stopped in category: ${category}`);
});
} else {
audio.pause();
audio.currentTime = 0; // Reset to beginning
audio._stopped = true; // Mark as stopped to prevent further playback
console.log(`[src debug] Stopping audio:`, audio.src);
// Don't clear src as it causes corruption - just pause and reset time
// Also remove from DOM if it exists
if (audio.parentNode) {
audio.parentNode.removeChild(audio);
}
this.categories[category].currentAudio = null;
console.log(`Audio immediately stopped in category: ${category}`);
}
// Clear any pending fade timeout
if (this.categories[category].fadeTimeout) {
clearTimeout(this.categories[category].fadeTimeout);
this.categories[category].fadeTimeout = null;
}
}
// Immediately stop all audio without fade
stopAllImmediate() {
console.log('Stopping all audio immediately...');
// Stop playlist first
this.stopPlaylist();
// Globally disable audio to prevent any new playback
this.audioEnabled = false;
for (const category in this.categories) {
if (this.categories[category].currentAudio) {
const audio = this.categories[category].currentAudio;
audio.pause();
audio.currentTime = 0;
audio._stopped = true; // Mark as stopped to prevent further playback
// Don't clear src as it causes corruption - just pause and reset
// Also remove from DOM if it exists
if (audio.parentNode) {
audio.parentNode.removeChild(audio);
}
this.categories[category].currentAudio = null;
console.log(`Forcibly stopped audio in category: ${category}`);
}
// Clear any pending fade timeouts
if (this.categories[category].fadeTimeout) {
clearTimeout(this.categories[category].fadeTimeout);
this.categories[category].fadeTimeout = null;
}
}
}
// Re-enable audio (call when game starts)
enableAudio() {
console.log('Audio re-enabled');
this.audioEnabled = true;
// Clear any stopped audio elements to prevent orphaned events
for (const category in this.categories) {
if (this.categories[category].currentAudio && this.categories[category].currentAudio._stopped) {
// Remove stopped audio elements completely
const stoppedAudio = this.categories[category].currentAudio;
if (stoppedAudio.parentNode) {
stoppedAudio.parentNode.removeChild(stoppedAudio);
}
this.categories[category].currentAudio = null;
console.log(`Cleared stopped audio element in category: ${category}`);
}
}
}
// Start continuous background audio playlist
startBackgroundPlaylist() {
if (!this.audioEnabled) {
console.log('Audio disabled - not starting playlist');
return;
}
// Get background audio files
const backgroundFiles = this.audioLibrary.background?.general || [];
if (backgroundFiles.length === 0) {
console.log('No background audio files available for playlist');
return;
}
// Shuffle the playlist
this.playlistQueue = [...backgroundFiles].sort(() => Math.random() - 0.5);
this.currentPlaylistIndex = 0;
this.isPlaylistMode = true;
console.log(`🎵 Starting background audio playlist with ${this.playlistQueue.length} tracks`);
this.playNextInPlaylist();
}
// Play next track in playlist
playNextInPlaylist() {
if (!this.isPlaylistMode || !this.audioEnabled) {
return;
}
if (this.playlistQueue.length === 0) {
console.log('Playlist is empty');
return;
}
// Stop current playlist audio if playing
if (this.currentPlaylistAudio) {
this.currentPlaylistAudio.pause();
this.currentPlaylistAudio.remove();
}
// Get next track (loop back to start if at end)
if (this.currentPlaylistIndex >= this.playlistQueue.length) {
this.currentPlaylistIndex = 0;
// Re-shuffle for variety
this.playlistQueue = [...this.playlistQueue].sort(() => Math.random() - 0.5);
console.log('🔄 Playlist completed, reshuffling and restarting');
}
const audioFile = this.playlistQueue[this.currentPlaylistIndex];
console.log(`🎵 Playing playlist track ${this.currentPlaylistIndex + 1}/${this.playlistQueue.length}: ${audioFile.name}`);
// Create audio element
this.currentPlaylistAudio = new Audio();
const audio = this.currentPlaylistAudio;
// Set up event handlers
audio.addEventListener('ended', () => {
console.log(`🎵 Track ended: ${audioFile.name}`);
this.currentPlaylistIndex++;
// Small delay before next track
setTimeout(() => this.playNextInPlaylist(), 500);
});
audio.addEventListener('error', (e) => {
console.error(`🎵 Playlist track failed: ${audioFile.name}`, e);
this.currentPlaylistIndex++;
// Try next track after error
setTimeout(() => this.playNextInPlaylist(), 1000);
});
// Set source and play
try {
// Use the same file URL conversion logic as the main playAudio method
let audioSrc = audioFile.path;
if (window.electronAPI && audioFile.path.match(/^[A-Za-z]:\//)) {
// Absolute Windows path - convert to file:// URL
audioSrc = `file:///${audioFile.path}`;
console.log(`🎵 Converted to file URL: ${audioSrc}`);
} else if (window.electronAPI && !audioFile.path.startsWith('file://')) {
// Relative path in Electron - make it relative to app root
audioSrc = audioFile.path;
console.log(`🎵 Using relative path: ${audioSrc}`);
}
audio.src = audioSrc;
audio.volume = this.getMasterVolume() * (this.getCategoryVolume('background') || 0.7);
audio.play().then(() => {
console.log(`🎵 Now playing: ${audioFile.name}`);
}).catch(error => {
console.error(`🎵 Failed to play playlist track: ${audioFile.name}`, error);
this.currentPlaylistIndex++;
setTimeout(() => this.playNextInPlaylist(), 1000);
});
} catch (error) {
console.error(`🎵 Error setting up playlist track: ${audioFile.name}`, error);
this.currentPlaylistIndex++;
setTimeout(() => this.playNextInPlaylist(), 1000);
}
}
// Pause playlist
pausePlaylist() {
if (this.currentPlaylistAudio && !this.currentPlaylistAudio.paused) {
this.currentPlaylistAudio.pause();
console.log('🎵 Playlist paused');
}
}
// Resume playlist
resumePlaylist() {
if (this.currentPlaylistAudio && this.currentPlaylistAudio.paused) {
this.currentPlaylistAudio.play().then(() => {
console.log('🎵 Playlist resumed');
}).catch(error => {
console.error('🎵 Failed to resume playlist', error);
// Try next track if current one fails
this.currentPlaylistIndex++;
setTimeout(() => this.playNextInPlaylist(), 500);
});
}
}
// Stop playlist completely
stopPlaylist() {
this.isPlaylistMode = false;
if (this.currentPlaylistAudio) {
this.currentPlaylistAudio.pause();
this.currentPlaylistAudio.src = '';
this.currentPlaylistAudio.remove();
this.currentPlaylistAudio = null;
}
this.playlistQueue = [];
this.currentPlaylistIndex = 0;
console.log('🎵 Playlist stopped');
}
// Stop all audio
stopAll(fadeOut = 0) {
for (const category in this.categories) {
this.stopCategory(category, fadeOut);
}
}
// Fade effects
fadeIn(audio, duration) {
const targetVolume = audio.volume;
audio.volume = 0;
const steps = 20;
const stepSize = targetVolume / steps;
const stepDelay = duration / steps;
let currentStep = 0;
const fadeInterval = setInterval(() => {
currentStep++;
audio.volume = Math.min(targetVolume, stepSize * currentStep);
if (currentStep >= steps) {
clearInterval(fadeInterval);
audio.volume = targetVolume;
}
}, stepDelay);
}
fadeOut(audio, duration, callback = null) {
const startVolume = audio.volume;
const steps = 20;
const stepSize = startVolume / steps;
const stepDelay = duration / steps;
let currentStep = 0;
const fadeInterval = setInterval(() => {
currentStep++;
audio.volume = Math.max(0, startVolume - (stepSize * currentStep));
if (currentStep >= steps) {
clearInterval(fadeInterval);
audio.volume = 0;
if (callback) callback();
}
}, stepDelay);
}
// Get current playing status
isPlaying(category) {
return this.categories[category]?.currentAudio &&
!this.categories[category].currentAudio.paused;
}
getCurrentlyPlaying() {
const playing = {};
for (const [category, config] of Object.entries(this.categories)) {
if (this.isPlaying(category)) {
playing[category] = {
src: config.currentAudio.src,
currentTime: config.currentAudio.currentTime,
duration: config.currentAudio.duration
};
}
}
return playing;
}
// Audio library info
getAvailableAudio() {
const available = {};
for (const [category, subcategories] of Object.entries(this.audioLibrary)) {
available[category] = {};
for (const [subcategory, files] of Object.entries(subcategories)) {
available[category][subcategory] = files.map(f => f.name);
}
}
return available;
}
// Preview audio (for settings menu)
async previewAudio(category, subcategory, fileName) {
// Stop any currently playing preview
this.stopCategory('preview');
// Play audio with temporary category
const audio = await this.playAudio(category, subcategory, fileName);
if (audio) {
// Auto-stop after 3 seconds for preview
setTimeout(() => {
if (audio && !audio.paused) {
audio.pause();
}
}, 3000);
}
return audio;
}
// Test audio playback - useful for debugging
async testAudio() {
console.log('Testing audio system...');
console.log('Audio library:', this.audioLibrary);
// Test a teasing audio file
if (this.audioLibrary.tasks && this.audioLibrary.tasks.teasing && this.audioLibrary.tasks.teasing.length > 0) {
console.log('Testing teasing audio...');
await this.playAudio('tasks', 'teasing');
} else {
console.log('No teasing audio files found');
}
}
// Convenient methods for specific audio types - now all use background audio
playTaskAudio(intensity = 'general', options = {}) {
return this.playBackgroundAudio(options);
}
playPunishmentAudio(type = 'general', options = {}) {
return this.playBackgroundAudio(options);
}
playRewardAudio(options = {}) {
return this.playBackgroundAudio(options);
}
playInstructionAudio(instruction, options = {}) {
return this.playBackgroundAudio(options);
}
// Main background audio playing method
playBackgroundAudio(options = {}) {
return this.playAudio('background', 'general', null, options);
}
// Enhanced playAudio with fallback support - simplified since we only have one category
async playAudioWithFallback(category, primarySubcategory, fallbackSubcategory, fileName = null, options = {}) {
try {
return await this.playAudio(category, primarySubcategory, fileName, options);
} catch (error) {
console.log(`Audio playback failed for ${category}/${primarySubcategory}`);
return null;
}
}
// Scan audio directories for files
async scanAudioDirectories() {
console.log('🔍 Scanning audio directories...');
const scannedFiles = [];
// Simplified directory structure - only background and ambient
const audioDirectories = [
'audio/background',
'audio/ambient'
];
// For Electron environment, we can use the existing readAudioDirectory API
if (window.electronAPI && window.electronAPI.readAudioDirectory) {
try {
for (const directory of audioDirectories) {
console.log(`Scanning directory: ${directory}`);
// Get the full path to the directory
const appPath = await window.electronAPI.getAppPath();
const fullPath = await window.electronAPI.pathJoin(appPath, directory);
// Check if directory exists before trying to read it
const dirExists = await window.electronAPI.fileExists(fullPath);
if (!dirExists) {
console.log(`Directory does not exist: ${fullPath}`);
continue;
}
const files = await window.electronAPI.readAudioDirectory(fullPath);
for (const file of files) {
scannedFiles.push({
name: file.name,
path: file.path,
title: file.title || file.name,
directory: directory,
enabled: true
});
}
}
console.log(`🔍 Found ${scannedFiles.length} audio files in directories`);
// Add scanned files to the audio library if they're not already in customAudio
if (scannedFiles.length > 0) {
await this.addScannedFilesToLibrary(scannedFiles);
}
return scannedFiles;
} catch (error) {
console.error('Error scanning audio directories:', error);
return [];
}
} else {
console.warn('Directory scanning not available - requires Electron environment');
console.log('💡 To use directory scanning:');
console.log(' 1. Run the app with: npm start');
console.log(' 2. Or manually add audio files via "Manage Audio" menu');
return [];
}
}
// Add scanned files to the audio library
async addScannedFilesToLibrary(scannedFiles) {
if (!window.game || !window.game.dataManager) {
console.warn('DataManager not available for adding scanned files');
return;
}
const customAudio = window.game.dataManager.get('customAudio') || {
background: [],
ambient: []
};
let addedCount = 0;
for (const file of scannedFiles) {
// Check if file is already in the library
const existsInBackground = customAudio.background.some(f => f.path === file.path);
const existsInAmbient = customAudio.ambient.some(f => f.path === file.path);
if (!existsInBackground && !existsInAmbient) {
// Determine which category to add to based on directory
let category = 'background'; // Default category
if (file.directory.includes('ambient')) {
category = 'ambient';
}
// Note: Since we removed effects, tasks, etc., everything else goes to background
customAudio[category].push({
name: file.name,
path: file.path,
enabled: true,
source: 'directory_scan'
});
addedCount++;
console.log(`Added ${file.name} to ${category} category`);
}
}
if (addedCount > 0) {
// Save updated audio library
window.game.dataManager.set('customAudio', customAudio);
// Refresh the audio library to include new files
await this.discoverAudioLibrary();
console.log(`✅ Added ${addedCount} new audio files to library`);
// Show success message to user
if (window.game && window.game.flashMessageManager) {
window.game.flashMessageManager.show(
`Audio Scan Complete: Added ${addedCount} audio files to library`,
'success'
);
}
} else {
console.log(' No new audio files found during scan');
if (window.game && window.game.flashMessageManager) {
window.game.flashMessageManager.show(
'Audio scan complete - no new files found',
'info'
);
}
}
}
// Refresh audio library - useful when user adds new audio files
async refreshAudioLibrary() {
console.log('🎵 Refreshing audio library...');
await this.discoverAudioLibrary();
console.log('🎵 Audio library refreshed');
}
// Get settings for UI
getSettings() {
return {
masterVolume: this.masterVolume,
categories: { ...this.categories },
available: this.getAvailableAudio(),
isInitialized: this.isInitialized
};
}
// Public method to trigger directory scan (for UI buttons)
async triggerDirectoryScan() {
console.log('🔍 Triggering audio directory scan from UI...');
return await this.scanAudioDirectories();
}
}