feat: align with upstream 1.5.9.9

This commit is contained in:
2026-09-12 18:21:11 +08:00
commit 34f0679117
805 changed files with 376275 additions and 0 deletions
+369
View File
@@ -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 || '编码失败'
})
}
}