feat: add smart audio copy and audio mix presets for configurable 5.1 downmixing

This commit is contained in:
Trevor Mears
2026-01-17 13:31:13 -08:00
parent 9919975663
commit c1f8077438
8 changed files with 80 additions and 13 deletions
+14
View File
@@ -548,6 +548,20 @@
<option value="low">Low (Fastest)</option>
</select>
</div>
<div class="setting-item">
<div class="setting-info" style="flex: 1;">
<span class="setting-label">Audio Mix Preset</span>
<span class="setting-hint">How multi-channel audio (5.1/7.1) is mixed down to stereo for browser
playback.</span>
</div>
<select id="setting-audio-mix" class="source-select" style="width: auto; min-width: 200px;">
<option value="auto">Auto (Smart Copy/ITU)</option>
<option value="itu">ITU-R BS.775 (Balanced)</option>
<option value="night">Night Mode (Dialogue Boost)</option>
<option value="cinematic">Cinematic (Wide)</option>
<option value="passthrough">Passthrough (Copy)</option>
</select>
</div>
</div>
<div class="settings-section">
+6 -1
View File
@@ -806,7 +806,12 @@ class VideoPlayer {
const statusText = videoMode === 'copy' ? 'Transcoding (Audio)' : 'Transcoding (Video)';
this.updateTranscodeStatus('transcoding', statusText);
const playlistUrl = await this.startTranscodeSession(streamUrl, { videoMode, videoCodec: info.video });
const playlistUrl = await this.startTranscodeSession(streamUrl, {
videoMode,
videoCodec: info.video,
audioCodec: info.audio,
audioChannels: info.audioChannels
});
this.currentUrl = playlistUrl; // Update currentUrl for HLS reload
this.playHls(playlistUrl);
+10
View File
@@ -149,6 +149,16 @@ class SettingsPage {
this.app.player.saveSettings();
});
// Audio Mix Preset
const audioMixSelect = document.getElementById('setting-audio-mix');
if (audioMixSelect) {
audioMixSelect.value = this.app.player?.settings?.audioMixPreset || 'auto';
audioMixSelect.addEventListener('change', () => {
this.app.player.settings.audioMixPreset = audioMixSelect.value;
this.app.player.saveSettings();
});
}
// Stream processing toggles
forceProxyToggle?.addEventListener('change', () => {
this.app.player.settings.forceProxy = forceProxyToggle.checked;
+6 -1
View File
@@ -330,7 +330,12 @@ class WatchPage {
const statusText = videoMode === 'copy' ? 'Transcoding (Audio)' : 'Transcoding (Video)';
this.updateTranscodeStatus('transcoding', statusText);
const playlistUrl = await this.startTranscodeSession(url, { videoMode, videoCodec: info.video });
const playlistUrl = await this.startTranscodeSession(url, {
videoMode,
videoCodec: info.video,
audioCodec: info.audio,
audioChannels: info.audioChannels
});
this.playHls(playlistUrl);
this.setVolumeFromStorage();
return;
+1
View File
@@ -76,6 +76,7 @@ function getDefaultSettings() {
hwEncoder: 'auto', // auto | nvenc | amf | qsv | vaapi | software
maxResolution: '1080p', // 4k | 1080p | 720p | 480p
quality: 'medium', // high | medium | low
audioMixPreset: 'auto', // auto | itu | night | cinematic | passthrough
// Probe cache settings
probeCacheTTL: 300, // 5 minutes for URL probe cache
seriesProbeCacheDays: 7 // 7 days for series episode probe cache
+1
View File
@@ -128,6 +128,7 @@ function analyzeProbeResult(probeResult, url) {
return {
video: videoCodec,
audio: audioCodec,
audioChannels: audioStream?.channels || 0, // For Smart Audio Copy
container: container,
compatible: compatible,
needsRemux: needsRemux,
+5 -2
View File
@@ -29,7 +29,7 @@ transcodeSession.startCleanupInterval();
* Body: { url: string, seekOffset?: number }
*/
router.post('/session', async (req, res) => {
const { url, seekOffset, videoMode, videoCodec } = req.body;
const { url, seekOffset, videoMode, videoCodec, audioCodec, audioChannels } = req.body;
if (!url) {
return res.status(400).json({ error: 'URL is required' });
@@ -47,8 +47,11 @@ router.post('/session', async (req, res) => {
hwEncoder: settings.hwEncoder || 'software',
maxResolution: settings.maxResolution || '1080p',
quality: settings.quality || 'medium',
audioMixPreset: settings.audioMixPreset || 'auto', // Audio downmix preset
videoMode: videoMode, // 'copy' or 'encode'
videoCodec: videoCodec // 'h264', 'hevc', etc.
videoCodec: videoCodec, // 'h264', 'hevc', etc.
audioCodec: audioCodec, // 'aac', 'ac3', etc.
audioChannels: audioChannels // number of channels (2=stereo)
});
await session.start();
+37 -9
View File
@@ -222,15 +222,43 @@ class TranscodeSession extends EventEmitter {
this.addVideoEncoderArgs(args, encoder);
}
// Audio: Transcode to AAC
args.push(
'-c:a', 'aac',
'-ar', '48000',
'-b:a', '192k',
// Smart Stereo Downmix (ITU-R BS.775 Standard + LFE):
// Balanced mix: FL/FR=100%, FC=70% (Unity), Surrounds=70%, LFE=50%
'-af', 'pan=stereo|FL=FL+0.707*FC+0.707*BL+0.5*LFE|FR=FR+0.707*FC+0.707*BR+0.5*LFE,aresample=async=1'
);
// Audio: Apply mix preset
const audioCodec = this.options.audioCodec?.toLowerCase() || 'unknown';
const audioChannels = this.options.audioChannels || 0;
const audioMixPreset = this.options.audioMixPreset || 'auto';
const isStereoAac = audioCodec.includes('aac') && audioChannels === 2;
// Define pan filter presets for 5.1 -> Stereo downmix
const AUDIO_MIX_FILTERS = {
// ITU-R BS.775 Standard: Mathematically balanced, transparent
itu: 'pan=stereo|FL=FL+0.707*FC+0.707*BL+0.5*LFE|FR=FR+0.707*FC+0.707*BR+0.5*LFE',
// Night Mode: Heavy dialogue boost, reduced bass/surrounds for quiet viewing
night: 'pan=stereo|FL=0.5*FL+1.2*FC+0.3*BL+0.1*LFE|FR=0.5*FR+1.2*FC+0.3*BR+0.1*LFE',
// Cinematic: Wide soundstage, immersive (original "dialogue boost" mix)
cinematic: 'pan=stereo|FL=FC+0.80*FL+0.60*BL+0.5*LFE|FR=FC+0.80*FR+0.60*BR+0.5*LFE'
};
if (audioMixPreset === 'passthrough') {
// Passthrough: Always copy audio, no processing
console.log(`[TranscodeSession ${this.id}] Audio: Passthrough (copy)`);
args.push('-c:a', 'copy');
} else if (audioMixPreset === 'auto' && isStereoAac) {
// Auto + Stereo AAC source: Smart copy
console.log(`[TranscodeSession ${this.id}] Audio: Auto (Smart Copy) - Source is Stereo AAC`);
args.push('-c:a', 'copy');
} else {
// Transcode to AAC with selected mix preset (default to ITU for 'auto')
const mixPreset = (audioMixPreset === 'auto') ? 'itu' : audioMixPreset;
const panFilter = AUDIO_MIX_FILTERS[mixPreset] || AUDIO_MIX_FILTERS.itu;
console.log(`[TranscodeSession ${this.id}] Audio: ${mixPreset.toUpperCase()} mix (${audioCodec} ${audioChannels}ch -> Stereo AAC)`);
args.push(
'-c:a', 'aac',
'-ar', '48000',
'-b:a', '192k',
'-af', `${panFilter},aresample=async=1`
);
}
// HLS output options
args.push(