audio
Version:
Audio loading, editing, and rendering for JavaScript
81 lines (68 loc) • 2.73 kB
JavaScript
import { dcOffsets, peakDb, rmsDb, lufsDb } from './loudness.js'
import audio from '../core.js'
const PRESETS = { streaming: -14, podcast: -16, broadcast: -23 }
/** DC removal — subtracts per-channel offset. Internal to normalize. */
audio.op('dc', {
hidden: true,
process: (chs, ctx) => {
let offsets = ctx.args[0]
if (typeof offsets === 'number') offsets = [offsets]
for (let c = 0; c < chs.length; c++) {
let d = offsets[c % offsets.length] || 0
if (Math.abs(d) < 1e-10) continue
for (let i = 0; i < chs[c].length; i++) chs[c][i] -= d
}
return chs
}
})
/** Clamp samples to ±limit (linear). Internal to normalize. */
audio.op('clamp', {
hidden: true,
process: (chs, ctx) => {
let limit = ctx.args[0]
for (let c = 0; c < chs.length; c++)
for (let i = 0; i < chs[c].length; i++)
chs[c][i] = Math.max(-limit, Math.min(limit, chs[c][i]))
return chs
}
})
audio.op('normalize', {
process: () => false,
resolve: (args, ctx) => {
let { stats, sampleRate } = ctx
if (!stats?.min) return null
let arg = args[0]
let mode = typeof arg === 'string' ? 'lufs' : ctx.mode || 'peak'
let targetDb = PRESETS[arg] ?? (typeof arg === 'number' ? arg : ctx.target ?? 0)
let totalCh = stats.min.length
let chs = ctx.channel != null ? (Array.isArray(ctx.channel) ? ctx.channel : [ctx.channel]) : Array.from({ length: totalCh }, (_, i) => i)
let dcOff = new Float64Array(totalCh)
if (ctx.dc !== false && stats.dc) dcOff = dcOffsets(stats, chs)
let hasDc = chs.some(c => Math.abs(dcOff[c]) > 1e-10)
let levelDb
if (mode === 'lufs') levelDb = lufsDb(stats, chs, sampleRate)
else if (mode === 'rms') levelDb = rmsDb(stats, chs, dcOff)
else levelDb = peakDb(stats, chs, dcOff)
if (levelDb == null) return false
let edits = []
if (hasDc) edits.push({ type: 'dc', args: [chs.map(c => dcOff[c])] })
// Ceiling mode: normalize peak then clip at ceiling level
if (ctx.ceiling != null) {
let peakLevel = peakDb(stats, chs, dcOff)
if (peakLevel == null) return false
edits.push({ type: 'gain', args: [targetDb - peakLevel] })
edits.push({ type: 'clamp', args: [10 ** (ctx.ceiling / 20)] })
} else {
edits.push({ type: 'gain', args: [targetDb - levelDb] })
}
return edits.length === 1 ? edits[0] : edits
},
call(std, arg) {
if (typeof arg === 'string' || typeof arg === 'number') return std.call(this, arg)
if (arg != null && typeof arg === 'object') {
let { target, mode, at, duration, channel, ...extra } = arg
return std.call(this, target, { mode, at, duration, channel, ...extra })
}
return std.call(this)
}
})