1
0

Add audio validation, seamless transitions, and quality improvements for music streaming

This commit is contained in:
2025-11-12 10:54:25 -05:00
parent 86a79d0cc0
commit 1f89e8d243
7 changed files with 191 additions and 64 deletions

View File

@@ -1,5 +1,60 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Cache for validated files to avoid re-validation on every request
let validationCache = null;
let cacheForPath = null;
/**
* Validate an audio file using ffprobe
* @param {string} filePath - Path to audio file
* @returns {boolean} True if file is valid and playable
*/
function validateAudioFile(filePath) {
try {
// Use ffprobe to check if file is valid and get audio info
const output = execSync(
`ffprobe -v error -select_streams a:0 -show_entries stream=codec_name,sample_rate,channels -of json "${filePath}"`,
{ encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
);
const data = JSON.parse(output);
// Check if audio stream exists and has valid properties
if (!data.streams || data.streams.length === 0) {
console.warn(`⚠️ Skipping ${path.basename(filePath)}: No audio stream found`);
return false;
}
const stream = data.streams[0];
// Validate codec
if (!stream.codec_name) {
console.warn(`⚠️ Skipping ${path.basename(filePath)}: Unknown codec`);
return false;
}
// Validate sample rate (should be reasonable)
const sampleRate = parseInt(stream.sample_rate);
if (!sampleRate || sampleRate < 8000 || sampleRate > 192000) {
console.warn(`⚠️ Skipping ${path.basename(filePath)}: Invalid sample rate (${sampleRate})`);
return false;
}
// Validate channels
const channels = parseInt(stream.channels);
if (!channels || channels < 1 || channels > 8) {
console.warn(`⚠️ Skipping ${path.basename(filePath)}: Invalid channel count (${channels})`);
return false;
}
return true;
} catch (error) {
console.warn(`⚠️ Skipping ${path.basename(filePath)}: Validation failed (${error.message})`);
return false;
}
}
/**
* Get a random music file from the music directory
@@ -24,17 +79,42 @@ function getRandomMusicFile(musicPath) {
}
/**
* Get all music files for playlist generation
* Get all valid music files for playlist generation (with caching)
* @param {string} musicPath - Path to music directory
* @returns {string[]} Array of music file paths
* @returns {string[]} Array of validated music file paths
*/
function getAllMusicFiles(musicPath) {
try {
if (!fs.existsSync(musicPath)) {
return [];
}
// Return cached results if available for this path
if (validationCache && cacheForPath === musicPath) {
console.log(`✅ Using cached validation results: ${validationCache.length} files`);
return validationCache;
}
const files = fs.readdirSync(musicPath).filter(f => f.endsWith('.ogg'));
return files.map(f => path.join(musicPath, f));
const filePaths = files.map(f => path.join(musicPath, f));
console.log(`🎵 Validating ${filePaths.length} audio files (first run, this will be cached)...`);
// Validate each file
const validFiles = filePaths.filter(validateAudioFile);
console.log(`${validFiles.length}/${filePaths.length} audio files passed validation`);
if (validFiles.length < filePaths.length) {
console.log(`⚠️ ${filePaths.length - validFiles.length} files were skipped due to issues`);
}
// Cache the results
validationCache = validFiles;
cacheForPath = musicPath;
console.log(`💾 Validation results cached for future requests`);
return validFiles;
} catch (error) {
console.error('Error getting music files:', error);
return [];
@@ -55,13 +135,13 @@ function shuffleArray(array) {
}
/**
* Create a playlist file for FFmpeg concat demuxer
* Create a playlist file for FFmpeg concat demuxer with optimized format
* @param {string} musicPath - Path to music directory
* @returns {{playlistFile: string, trackCount: number}|null} Playlist info or null
*/
function createPlaylist(musicPath) {
const allMusicFiles = getAllMusicFiles(musicPath);
console.log(`Found ${allMusicFiles.length} music files in ${musicPath}`);
console.log(`Found ${allMusicFiles.length} validated music files`);
if (allMusicFiles.length === 0) {
return null;
@@ -75,18 +155,27 @@ function createPlaylist(musicPath) {
const repetitions = Math.max(20, Math.ceil(480 / allMusicFiles.length)); // At least 480 tracks (~24hrs)
const playlistLines = [];
// Use FFmpeg concat demuxer format with improved options for smooth transitions
playlistLines.push('ffconcat version 1.0');
for (let i = 0; i < repetitions; i++) {
// Re-shuffle each repetition for more variety
const shuffled = shuffleArray([...allMusicFiles]);
shuffled.forEach(f => playlistLines.push(`file '${f}'`));
shuffled.forEach(f => {
// Escape single quotes in file paths for concat format
const escapedPath = f.replace(/'/g, "'\\''");
playlistLines.push(`file '${escapedPath}'`);
// Add duration metadata hint for smoother transitions (helps FFmpeg prebuffer)
playlistLines.push('# Auto-generated entry');
});
}
fs.writeFileSync(playlistFile, playlistLines.join('\n'));
console.log(`Created playlist with ${allMusicFiles.length} tracks x${repetitions} repetitions (~${playlistLines.length} total tracks)`);
console.log(`Created seamless playlist: ${allMusicFiles.length} tracks × ${repetitions} repetitions = ${allMusicFiles.length * repetitions} total tracks`);
return {
playlistFile,
trackCount: playlistLines.length
trackCount: allMusicFiles.length * repetitions
};
}