mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-05-25 21:01:31 +00:00
feat: implement smart speed phase 3 silence compression
This commit is contained in:
parent
ebff884562
commit
4299fdce59
5 changed files with 568 additions and 7 deletions
122
client/players/smart-speed/SilenceCompressorProcessor.js
Normal file
122
client/players/smart-speed/SilenceCompressorProcessor.js
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
class SilenceCompressorProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super()
|
||||
this.regions = []
|
||||
this.ratio = 1.0
|
||||
this.totalCompressedMs = 0
|
||||
this.rampDurationSec = 0.005 // 5ms
|
||||
|
||||
this.port.onmessage = (event) => {
|
||||
const msg = event.data
|
||||
if (msg.type === 'set-regions') {
|
||||
this.regions = msg.regions.filter(r => (r.end - r.start) >= 200)
|
||||
} else if (msg.type === 'set-ratio') {
|
||||
this.ratio = msg.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getActiveRegion(timeMs) {
|
||||
for (const r of this.regions) {
|
||||
if (timeMs >= r.start && timeMs <= r.end) return r
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
calculateRampGain(timeMs, region) {
|
||||
const rampMs = this.rampDurationSec * 1000
|
||||
|
||||
// Entry ramp (0 -> 1)
|
||||
if (timeMs - region.start < rampMs) {
|
||||
return (timeMs - region.start) / rampMs
|
||||
}
|
||||
|
||||
// Exit ramp (1 -> 0)
|
||||
if (region.end - timeMs < rampMs) {
|
||||
return (region.end - timeMs) / rampMs
|
||||
}
|
||||
|
||||
return 1.0
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
const input = inputs[0]
|
||||
const output = outputs[0]
|
||||
|
||||
if (!input || !input.length || !output || !output.length) return true
|
||||
|
||||
const numChannels = input.length
|
||||
const numFrames = input[0].length
|
||||
const sampleRateC = typeof sampleRate !== 'undefined' ? sampleRate : 48000
|
||||
// Use currentTime if available, otherwise fallback to 0 (for tests)
|
||||
const currentTimeSec = typeof currentTime !== 'undefined' ? currentTime : 0
|
||||
|
||||
let outputIndex = 0
|
||||
let inputIndex = 0
|
||||
let savedSecThisBlock = 0
|
||||
|
||||
while (inputIndex < numFrames) {
|
||||
const sampleTimeSec = currentTimeSec + (inputIndex / sampleRateC)
|
||||
const sampleTimeMs = sampleTimeSec * 1000
|
||||
|
||||
const region = this.getActiveRegion(sampleTimeMs)
|
||||
|
||||
let step = 1.0
|
||||
let rampGain = 1.0
|
||||
|
||||
if (region && this.ratio > 1.0) {
|
||||
step = this.ratio
|
||||
rampGain = this.calculateRampGain(sampleTimeMs, region)
|
||||
}
|
||||
|
||||
// If taking this step exceeds the input buffer, we must stop
|
||||
if (inputIndex >= numFrames) break
|
||||
|
||||
const intIndex = Math.floor(inputIndex)
|
||||
const frac = inputIndex - intIndex
|
||||
|
||||
for (let c = 0; c < numChannels; c++) {
|
||||
const inChannel = input[c]
|
||||
const outChannel = output[c]
|
||||
|
||||
let sample = inChannel[intIndex]
|
||||
if (frac > 0 && intIndex + 1 < numFrames) {
|
||||
sample = sample + frac * (inChannel[intIndex + 1] - sample)
|
||||
}
|
||||
|
||||
if (outputIndex < numFrames) {
|
||||
outChannel[outputIndex] = sample * rampGain
|
||||
}
|
||||
}
|
||||
|
||||
inputIndex += step
|
||||
outputIndex += 1
|
||||
|
||||
if (step > 1.0) {
|
||||
savedSecThisBlock += (step - 1.0) / sampleRateC
|
||||
}
|
||||
}
|
||||
|
||||
// Fill the rest of the output buffer with 0s if we compressed
|
||||
for (let c = 0; c < numChannels; c++) {
|
||||
for (let i = outputIndex; i < numFrames; i++) {
|
||||
output[c][i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
if (savedSecThisBlock > 0) {
|
||||
this.totalCompressedMs += savedSecThisBlock * 1000
|
||||
this.port.postMessage({ type: 'time-saved', ms: this.totalCompressedMs })
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof registerProcessor !== 'undefined') {
|
||||
registerProcessor('silence-compressor', SilenceCompressorProcessor)
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = SilenceCompressorProcessor
|
||||
}
|
||||
89
client/players/smart-speed/TimeMapper.js
Normal file
89
client/players/smart-speed/TimeMapper.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
class TimeMapper {
|
||||
constructor(silenceRegions = [], compressionRatio = 1.0) {
|
||||
this.ratio = compressionRatio
|
||||
// Only keep regions >= 200ms
|
||||
this.regions = silenceRegions.filter(r => (r.end - r.start) >= 200)
|
||||
|
||||
// Calculate compressed durations and cumulative time saved
|
||||
this.processedRegions = []
|
||||
let accumulatedSaved = 0
|
||||
|
||||
for (const r of this.regions) {
|
||||
const originalDuration = r.end - r.start
|
||||
const compressedDuration = this.ratio === 0 ? 0 : originalDuration / this.ratio
|
||||
const saved = originalDuration - compressedDuration
|
||||
|
||||
this.processedRegions.push({
|
||||
...r,
|
||||
originalDuration,
|
||||
compressedDuration,
|
||||
saved,
|
||||
accumulatedSavedBefore: accumulatedSaved
|
||||
})
|
||||
|
||||
accumulatedSaved += saved
|
||||
}
|
||||
|
||||
this._totalTimeSaved = accumulatedSaved
|
||||
}
|
||||
|
||||
wallClockToAudio(wallMs) {
|
||||
if (this.ratio === 1.0 || this.regions.length === 0) return wallMs
|
||||
|
||||
let audioMs = wallMs
|
||||
|
||||
for (const r of this.processedRegions) {
|
||||
// The start time of this region in wall-clock time
|
||||
const regionWallStart = r.start - r.accumulatedSavedBefore
|
||||
|
||||
if (wallMs < regionWallStart) {
|
||||
// Before this region, no more accumulated saved to add
|
||||
break
|
||||
}
|
||||
|
||||
const regionWallEnd = regionWallStart + r.compressedDuration
|
||||
|
||||
if (wallMs <= regionWallEnd) {
|
||||
// Inside the compressed region
|
||||
const timeSpentInRegionWall = wallMs - regionWallStart
|
||||
const timeSpentInRegionAudio = timeSpentInRegionWall * this.ratio
|
||||
return r.start + timeSpentInRegionAudio
|
||||
}
|
||||
|
||||
// After this region, we add the total time saved by this region
|
||||
audioMs = wallMs + (r.accumulatedSavedBefore + r.saved)
|
||||
}
|
||||
|
||||
return audioMs
|
||||
}
|
||||
|
||||
audioToWallClock(audioMs) {
|
||||
if (this.ratio === 1.0 || this.regions.length === 0) return audioMs
|
||||
|
||||
let wallMs = audioMs
|
||||
|
||||
for (const r of this.processedRegions) {
|
||||
if (audioMs < r.start) {
|
||||
break
|
||||
}
|
||||
|
||||
if (audioMs <= r.end) {
|
||||
// Inside the region
|
||||
const timeSpentInRegionAudio = audioMs - r.start
|
||||
const timeSpentInRegionWall = timeSpentInRegionAudio / this.ratio
|
||||
return r.start - r.accumulatedSavedBefore + timeSpentInRegionWall
|
||||
}
|
||||
|
||||
// After the region
|
||||
wallMs = audioMs - (r.accumulatedSavedBefore + r.saved)
|
||||
}
|
||||
|
||||
return wallMs
|
||||
}
|
||||
|
||||
totalTimeSaved() {
|
||||
return this._totalTimeSaved
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TimeMapper
|
||||
Loading…
Add table
Add a link
Reference in a new issue