feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
import { createMp3Encoder } from 'wasm-media-encoders'
|
||||
|
||||
const clamp = (value, min, max) => Math.max(min, Math.min(max, value))
|
||||
|
||||
const normalizeStereo = (left, right, targetDb) => {
|
||||
const targetGain = Math.pow(10, targetDb / 20)
|
||||
let maxPeak = 0
|
||||
for (let i = 0; i < left.length; i++) {
|
||||
const l = Math.abs(left[i])
|
||||
const r = Math.abs(right[i])
|
||||
if (l > maxPeak) maxPeak = l
|
||||
if (r > maxPeak) maxPeak = r
|
||||
}
|
||||
if (maxPeak === 0) return
|
||||
const gain = targetGain / maxPeak
|
||||
for (let i = 0; i < left.length; i++) {
|
||||
left[i] *= gain
|
||||
right[i] *= gain
|
||||
}
|
||||
}
|
||||
|
||||
const resampleLinear = (source, sourceRate, targetRate) => {
|
||||
if (sourceRate === targetRate) return source
|
||||
const ratio = targetRate / sourceRate
|
||||
const targetLength = Math.max(1, Math.floor(source.length * ratio))
|
||||
const output = new Float32Array(targetLength)
|
||||
for (let i = 0; i < targetLength; i++) {
|
||||
const origin = i / ratio
|
||||
const left = Math.floor(origin)
|
||||
const right = Math.min(source.length - 1, left + 1)
|
||||
const weight = origin - left
|
||||
output[i] = source[left] * (1 - weight) + source[right] * weight
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
const interleave = (left, right) => {
|
||||
const result = new Float32Array(left.length + right.length)
|
||||
let index = 0
|
||||
for (let i = 0; i < left.length; i++) {
|
||||
result[index++] = left[i]
|
||||
result[index++] = right[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const writeString = (view, offset, text) => {
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
view.setUint8(offset + i, text.charCodeAt(i))
|
||||
}
|
||||
}
|
||||
|
||||
const floatTo16BitPCM = (view, offset, input) => {
|
||||
for (let i = 0; i < input.length; i++, offset += 2) {
|
||||
const s = clamp(input[i], -1, 1)
|
||||
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true)
|
||||
}
|
||||
}
|
||||
|
||||
const writeWavHeader = (sampleRate, totalSamples) => {
|
||||
const numChannels = 2
|
||||
const bitDepth = 16
|
||||
const bytesPerSample = bitDepth / 8
|
||||
const blockAlign = numChannels * bytesPerSample
|
||||
const dataSize = totalSamples * numChannels * bytesPerSample
|
||||
const buffer = new ArrayBuffer(44)
|
||||
const view = new DataView(buffer)
|
||||
writeString(view, 0, 'RIFF')
|
||||
view.setUint32(4, 36 + dataSize, true)
|
||||
writeString(view, 8, 'WAVE')
|
||||
writeString(view, 12, 'fmt ')
|
||||
view.setUint32(16, 16, true)
|
||||
view.setUint16(20, 1, true)
|
||||
view.setUint16(22, numChannels, true)
|
||||
view.setUint32(24, sampleRate, true)
|
||||
view.setUint32(28, sampleRate * blockAlign, true)
|
||||
view.setUint16(32, blockAlign, true)
|
||||
view.setUint16(34, bitDepth, true)
|
||||
writeString(view, 36, 'data')
|
||||
view.setUint32(40, dataSize, true)
|
||||
return view
|
||||
}
|
||||
|
||||
const encodeWavChunk = (left, right) => {
|
||||
const bytesPerSample = 2
|
||||
const numChannels = 2
|
||||
const buffer = new ArrayBuffer(left.length * numChannels * bytesPerSample)
|
||||
const view = new DataView(buffer)
|
||||
let offset = 0
|
||||
for (let i = 0; i < left.length; i++) {
|
||||
const l = clamp(left[i], -1, 1)
|
||||
const r = clamp(right[i], -1, 1)
|
||||
view.setInt16(offset, l < 0 ? l * 0x8000 : l * 0x7fff, true)
|
||||
offset += 2
|
||||
view.setInt16(offset, r < 0 ? r * 0x8000 : r * 0x7fff, true)
|
||||
offset += 2
|
||||
}
|
||||
return new Uint8Array(buffer)
|
||||
}
|
||||
|
||||
const encodeWavBlob = (left, right, sampleRate) => {
|
||||
const interleaved = interleave(left, right)
|
||||
const bytesPerSample = 2
|
||||
const header = writeWavHeader(sampleRate, left.length)
|
||||
const buffer = new ArrayBuffer(44 + interleaved.length * bytesPerSample)
|
||||
const view = new DataView(buffer)
|
||||
new Uint8Array(buffer).set(new Uint8Array(header.buffer), 0)
|
||||
floatTo16BitPCM(view, 44, interleaved)
|
||||
return new Blob([view], { type: 'audio/wav' })
|
||||
}
|
||||
|
||||
const encodeMp3Blob = async (left, right, sampleRate) => {
|
||||
const encoder = await createMp3Encoder()
|
||||
encoder.configure({
|
||||
sampleRate,
|
||||
channels: 2,
|
||||
bitrate: 192,
|
||||
outputSampleRate: sampleRate
|
||||
})
|
||||
const chunkSize = sampleRate * 2
|
||||
let processed = 0
|
||||
const total = left.length
|
||||
const chunks = []
|
||||
while (processed < total) {
|
||||
const end = Math.min(processed + chunkSize, total)
|
||||
const leftChunk = left.subarray(processed, end)
|
||||
const rightChunk = right.subarray(processed, end)
|
||||
const chunk = encoder.encode([leftChunk, rightChunk])
|
||||
if (chunk.length > 0) {
|
||||
const copy = new Uint8Array(chunk.length)
|
||||
copy.set(chunk)
|
||||
chunks.push(copy)
|
||||
}
|
||||
processed = end
|
||||
postMessage({
|
||||
type: 'progress',
|
||||
stage: 'encode',
|
||||
value: Math.round((processed / total) * 100)
|
||||
})
|
||||
}
|
||||
const finalized = encoder.finalize()
|
||||
if (finalized.length > 0) {
|
||||
const copy = new Uint8Array(finalized.length)
|
||||
copy.set(finalized)
|
||||
chunks.push(copy)
|
||||
}
|
||||
return new Blob(chunks, { type: 'audio/mp3' })
|
||||
}
|
||||
|
||||
let streamState = null
|
||||
|
||||
const resetStreamState = () => {
|
||||
streamState = null
|
||||
}
|
||||
|
||||
const ensureMp3StreamEncoder = async () => {
|
||||
if (!streamState.encoder) {
|
||||
const encoder = await createMp3Encoder()
|
||||
encoder.configure({
|
||||
sampleRate: streamState.sampleRate,
|
||||
channels: 2,
|
||||
bitrate: 192,
|
||||
outputSampleRate: streamState.sampleRate
|
||||
})
|
||||
streamState.encoder = encoder
|
||||
}
|
||||
return streamState.encoder
|
||||
}
|
||||
|
||||
const appendEncodedChunk = (chunk) => {
|
||||
if (chunk.length === 0) return
|
||||
const copy = new Uint8Array(chunk.length)
|
||||
copy.set(chunk)
|
||||
streamState.chunks.push(copy)
|
||||
}
|
||||
|
||||
const encodeStreamTrack = async (track, requestId) => {
|
||||
let left = new Float32Array(track.left)
|
||||
let right = new Float32Array(track.right)
|
||||
const sharedChannels = left.buffer === right.buffer
|
||||
if (streamState.normalize) {
|
||||
if (sharedChannels) {
|
||||
normalizeStereo(left, new Float32Array(left.length), streamState.targetDb)
|
||||
} else {
|
||||
normalizeStereo(left, right, streamState.targetDb)
|
||||
}
|
||||
}
|
||||
if (track.sampleRate !== streamState.sampleRate) {
|
||||
if (sharedChannels) {
|
||||
left = resampleLinear(left, track.sampleRate, streamState.sampleRate)
|
||||
right = left
|
||||
} else {
|
||||
left = resampleLinear(left, track.sampleRate, streamState.sampleRate)
|
||||
right = resampleLinear(right, track.sampleRate, streamState.sampleRate)
|
||||
}
|
||||
}
|
||||
|
||||
const chunkSize = streamState.sampleRate * 2
|
||||
let processed = 0
|
||||
while (processed < left.length) {
|
||||
const end = Math.min(processed + chunkSize, left.length)
|
||||
const leftChunk = left.subarray(processed, end)
|
||||
const rightChunk = right.subarray(processed, end)
|
||||
if (streamState.format === 'wav') {
|
||||
streamState.chunks.push(encodeWavChunk(leftChunk, rightChunk))
|
||||
} else {
|
||||
const encoder = await ensureMp3StreamEncoder()
|
||||
appendEncodedChunk(encoder.encode([leftChunk, rightChunk]))
|
||||
}
|
||||
processed = end
|
||||
postMessage({
|
||||
type: 'progress',
|
||||
stage: 'encode',
|
||||
value: Math.round((processed / left.length) * 100),
|
||||
requestId
|
||||
})
|
||||
}
|
||||
|
||||
streamState.totalSamples += left.length
|
||||
postMessage({
|
||||
type: 'trackDone',
|
||||
requestId
|
||||
})
|
||||
}
|
||||
|
||||
const finishStream = async (requestId) => {
|
||||
let blob
|
||||
if (streamState.format === 'wav') {
|
||||
blob = new Blob([writeWavHeader(streamState.sampleRate, streamState.totalSamples), ...streamState.chunks], {
|
||||
type: 'audio/wav'
|
||||
})
|
||||
} else {
|
||||
const encoder = await ensureMp3StreamEncoder()
|
||||
appendEncodedChunk(encoder.finalize())
|
||||
blob = new Blob(streamState.chunks, { type: 'audio/mp3' })
|
||||
}
|
||||
|
||||
const sampleRate = streamState.sampleRate
|
||||
resetStreamState()
|
||||
postMessage({
|
||||
type: 'done',
|
||||
blob,
|
||||
sampleRate,
|
||||
requestId
|
||||
})
|
||||
}
|
||||
|
||||
self.onmessage = async (event) => {
|
||||
const payload = event.data
|
||||
if (payload?.cmd === 'startStream') {
|
||||
resetStreamState()
|
||||
streamState = {
|
||||
format: payload.format === 'wav' ? 'wav' : 'mp3',
|
||||
normalize: Boolean(payload.normalize),
|
||||
targetDb: payload.targetDb,
|
||||
sampleRate: Math.min(payload.sampleRate || 44100, payload.format === 'mp3' ? 48000 : 96000),
|
||||
chunks: [],
|
||||
totalSamples: 0,
|
||||
encoder: null
|
||||
}
|
||||
postMessage({
|
||||
type: 'ready',
|
||||
requestId: payload.requestId
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (payload?.cmd === 'appendTrack') {
|
||||
try {
|
||||
if (!streamState) throw new Error('编码任务未初始化')
|
||||
await encodeStreamTrack(payload.track, payload.requestId)
|
||||
} catch (error) {
|
||||
postMessage({
|
||||
type: 'error',
|
||||
message: error?.message || '编码失败',
|
||||
requestId: payload.requestId
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (payload?.cmd === 'finishStream') {
|
||||
try {
|
||||
if (!streamState) throw new Error('编码任务未初始化')
|
||||
await finishStream(payload.requestId)
|
||||
} catch (error) {
|
||||
postMessage({
|
||||
type: 'error',
|
||||
message: error?.message || '编码失败',
|
||||
requestId: payload.requestId
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (payload?.cmd === 'cancelStream') {
|
||||
resetStreamState()
|
||||
return
|
||||
}
|
||||
|
||||
if (payload?.cmd !== 'encode' || !payload.tracks?.length) return
|
||||
try {
|
||||
let baseSampleRate = Math.max(...payload.tracks.map((track) => track.sampleRate))
|
||||
if (payload.format === 'mp3' && baseSampleRate > 48000) {
|
||||
baseSampleRate = 48000
|
||||
}
|
||||
const normalizedTracks = payload.tracks.map((track, index) => {
|
||||
let left = new Float32Array(track.left)
|
||||
let right = new Float32Array(track.right)
|
||||
if (payload.normalize) {
|
||||
normalizeStereo(left, right, payload.targetDb)
|
||||
}
|
||||
if (track.sampleRate !== baseSampleRate) {
|
||||
left = resampleLinear(left, track.sampleRate, baseSampleRate)
|
||||
right = resampleLinear(right, track.sampleRate, baseSampleRate)
|
||||
}
|
||||
postMessage({
|
||||
type: 'progress',
|
||||
stage: 'prepare',
|
||||
value: Math.round(((index + 1) / payload.tracks.length) * 100)
|
||||
})
|
||||
return {
|
||||
left,
|
||||
right
|
||||
}
|
||||
})
|
||||
const totalLength = normalizedTracks.reduce((sum, item) => sum + item.left.length, 0)
|
||||
const mergedLeft = new Float32Array(totalLength)
|
||||
const mergedRight = new Float32Array(totalLength)
|
||||
let offset = 0
|
||||
for (let i = 0; i < normalizedTracks.length; i++) {
|
||||
const item = normalizedTracks[i]
|
||||
mergedLeft.set(item.left, offset)
|
||||
mergedRight.set(item.right, offset)
|
||||
offset += item.left.length
|
||||
postMessage({
|
||||
type: 'progress',
|
||||
stage: 'merge',
|
||||
value: Math.round(((i + 1) / normalizedTracks.length) * 100)
|
||||
})
|
||||
}
|
||||
let blob
|
||||
if (payload.format === 'wav') {
|
||||
postMessage({
|
||||
type: 'progress',
|
||||
stage: 'encode',
|
||||
value: 20
|
||||
})
|
||||
blob = encodeWavBlob(mergedLeft, mergedRight, baseSampleRate)
|
||||
postMessage({
|
||||
type: 'progress',
|
||||
stage: 'encode',
|
||||
value: 100
|
||||
})
|
||||
} else {
|
||||
blob = await encodeMp3Blob(mergedLeft, mergedRight, baseSampleRate)
|
||||
}
|
||||
postMessage({
|
||||
type: 'done',
|
||||
blob,
|
||||
sampleRate: baseSampleRate
|
||||
})
|
||||
} catch (error) {
|
||||
postMessage({
|
||||
type: 'error',
|
||||
message: error?.message || '编码失败'
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user