Revert "style: lint"

This reverts commit 98f40d7d0b.
This commit is contained in:
Neko Ayaka
2026-08-26 20:13:10 +08:00
parent cfcfc513ef
commit 146b3da65a
1625 changed files with 75440 additions and 75453 deletions
+172 -172
View File
@@ -21,145 +21,57 @@ const activeWorkletNodes = new Set<AudioWorkletNode>()
const listeners = new Set<(state: State) => void>()
export interface State {
currentTime: number
error: string
isInitializing: boolean
isReady: boolean
sampleRate: number
state: AudioContextState
error: string
isInitializing: boolean
workletLoaded: boolean
currentTime: number
state: AudioContextState
}
export interface WorkletOptions {
bufferSize?: number
channels?: number
converterType?: number
inputSampleRate?: number
outputSampleRate?: number
channels?: number
converterType?: number
bufferSize?: number
}
export async function cleanupAudioContext() {
// Disconnect all active nodes
activeSources.forEach(source => source.disconnect())
activeGainNodes.forEach(gainNode => gainNode.disconnect())
activeAnalyzers.forEach(analyser => analyser.disconnect())
activeWorkletNodes.forEach(worklet => worklet.disconnect())
// Clear sets
activeSources.clear()
activeGainNodes.clear()
activeAnalyzers.clear()
activeWorkletNodes.clear()
// Close context
if (context && context.state !== 'closed') {
await context.close()
}
context = undefined
isReady = false
workletLoaded = false
error = ''
notifyListeners()
}
export function createAudioAnalyser(options?: Partial<{
fftSize: number
maxDecibels: number
minDecibels: number
smoothingTimeConstant: number
}>): AnalyserNode {
if (!context || !isReady) {
throw new Error('AudioContext not initialized')
}
const analyser = context.createAnalyser()
if (options?.fftSize)
analyser.fftSize = options.fftSize
if (options?.smoothingTimeConstant !== undefined) {
analyser.smoothingTimeConstant = options.smoothingTimeConstant
}
if (options?.minDecibels !== undefined)
analyser.minDecibels = options.minDecibels
if (options?.maxDecibels !== undefined)
analyser.maxDecibels = options.maxDecibels
activeAnalyzers.add(analyser)
return analyser
}
export function createAudioGainNode(initialGain: number = 1): GainNode {
if (!context || !isReady) {
throw new Error('AudioContext not initialized')
}
const gainNode = context.createGain()
gainNode.gain.value = initialGain
activeGainNodes.add(gainNode)
return gainNode
}
export function createAudioSource(mediaStream: MediaStream): MediaStreamAudioSourceNode {
if (!context || !isReady) {
throw new Error('AudioContext not initialized')
}
const source = context.createMediaStreamSource(mediaStream)
activeSources.add(source)
return source
}
export function createResamplingWorkletNode(
inputNode: AudioNode,
options: WorkletOptions = {},
): AudioWorkletNode {
if (!context || !isReady || !workletLoaded) {
throw new Error('AudioContext or worklets not ready')
}
const workletOptions = {
bufferSize: 4096,
channels: 1,
converterType: 2, // SRC_SINC_MEDIUM_QUALITY
inputSampleRate: sampleRate,
outputSampleRate: 16000,
...options,
}
const workletNode = new AudioWorkletNode(context, 'resampling-processor', {
channelCount: workletOptions.channels,
numberOfInputs: 1,
numberOfOutputs: 1,
processorOptions: workletOptions,
})
// Connect input to worklet
inputNode.connect(workletNode)
activeWorkletNodes.add(workletNode)
return workletNode
}
export function getAudioContext(): AudioContext | undefined {
return context
}
export function getAudioContextState(): State {
return {
currentTime: context?.currentTime ?? 0,
error,
isInitializing,
function notifyListeners() {
const state: State = {
isReady,
sampleRate,
state: context?.state ?? 'closed',
error,
isInitializing,
workletLoaded,
currentTime: context?.currentTime ?? 0,
state: context?.state ?? 'closed',
}
listeners.forEach((listener) => {
try {
listener(state)
}
catch (err) {
console.error('AudioContext state listener error:', err)
}
})
}
export function getCurrentTime(): number {
return context?.currentTime ?? 0
async function loadWorklets() {
if (!context || workletLoaded)
return
try {
await context.audioWorklet.addModule(ProcessorWorkletURL)
await context.audioWorklet.addModule(LibsamplerateWorkletURL)
workletLoaded = true
}
catch (err) {
console.error('Failed to load AudioWorklets:', err)
throw new Error(`Worklet loading failed: ${err}`)
}
}
export async function initializeAudioContext(requestedSampleRate: number = 48000): Promise<AudioContext> {
@@ -231,18 +143,51 @@ export async function initializeAudioContext(requestedSampleRate: number = 48000
}
}
export function isAudioContextReady(): boolean {
return isReady
export function createAudioSource(mediaStream: MediaStream): MediaStreamAudioSourceNode {
if (!context || !isReady) {
throw new Error('AudioContext not initialized')
}
const source = context.createMediaStreamSource(mediaStream)
activeSources.add(source)
return source
}
export function removeAudioAnalyser(analyser: AnalyserNode) {
analyser.disconnect()
activeAnalyzers.delete(analyser)
export function createAudioAnalyser(options?: Partial<{
fftSize: number
smoothingTimeConstant: number
minDecibels: number
maxDecibels: number
}>): AnalyserNode {
if (!context || !isReady) {
throw new Error('AudioContext not initialized')
}
const analyser = context.createAnalyser()
if (options?.fftSize)
analyser.fftSize = options.fftSize
if (options?.smoothingTimeConstant !== undefined) {
analyser.smoothingTimeConstant = options.smoothingTimeConstant
}
if (options?.minDecibels !== undefined)
analyser.minDecibels = options.minDecibels
if (options?.maxDecibels !== undefined)
analyser.maxDecibels = options.maxDecibels
activeAnalyzers.add(analyser)
return analyser
}
export function removeAudioGainNode(gainNode: GainNode) {
gainNode.disconnect()
activeGainNodes.delete(gainNode)
export function createAudioGainNode(initialGain: number = 1): GainNode {
if (!context || !isReady) {
throw new Error('AudioContext not initialized')
}
const gainNode = context.createGain()
gainNode.gain.value = initialGain
activeGainNodes.add(gainNode)
return gainNode
}
export function removeAudioSource(source: MediaStreamAudioSourceNode) {
@@ -250,22 +195,14 @@ export function removeAudioSource(source: MediaStreamAudioSourceNode) {
activeSources.delete(source)
}
export function removeWorkletNode(node: AudioWorkletNode) {
node.disconnect()
activeWorkletNodes.delete(node)
export function removeAudioGainNode(gainNode: GainNode) {
gainNode.disconnect()
activeGainNodes.delete(gainNode)
}
export async function resumeAudioContext() {
if (context && context.state === 'suspended') {
await context.resume()
notifyListeners()
}
}
// Event subscription
export function subscribeToAudioContext(listener: (state: State) => void): () => void {
listeners.add(listener)
return () => listeners.delete(listener)
export function removeAudioAnalyser(analyser: AnalyserNode) {
analyser.disconnect()
activeAnalyzers.delete(analyser)
}
export async function suspendAudioContext() {
@@ -275,40 +212,103 @@ export async function suspendAudioContext() {
}
}
async function loadWorklets() {
if (!context || workletLoaded)
return
try {
await context.audioWorklet.addModule(ProcessorWorkletURL)
await context.audioWorklet.addModule(LibsamplerateWorkletURL)
workletLoaded = true
}
catch (err) {
console.error('Failed to load AudioWorklets:', err)
throw new Error(`Worklet loading failed: ${err}`)
export async function resumeAudioContext() {
if (context && context.state === 'suspended') {
await context.resume()
notifyListeners()
}
}
function notifyListeners() {
const state: State = {
currentTime: context?.currentTime ?? 0,
error,
isInitializing,
export function createResamplingWorkletNode(
inputNode: AudioNode,
options: WorkletOptions = {},
): AudioWorkletNode {
if (!context || !isReady || !workletLoaded) {
throw new Error('AudioContext or worklets not ready')
}
const workletOptions = {
inputSampleRate: sampleRate,
outputSampleRate: 16000,
channels: 1,
converterType: 2, // SRC_SINC_MEDIUM_QUALITY
bufferSize: 4096,
...options,
}
const workletNode = new AudioWorkletNode(context, 'resampling-processor', {
numberOfInputs: 1,
numberOfOutputs: 1,
channelCount: workletOptions.channels,
processorOptions: workletOptions,
})
// Connect input to worklet
inputNode.connect(workletNode)
activeWorkletNodes.add(workletNode)
return workletNode
}
export function removeWorkletNode(node: AudioWorkletNode) {
node.disconnect()
activeWorkletNodes.delete(node)
}
export async function cleanupAudioContext() {
// Disconnect all active nodes
activeSources.forEach(source => source.disconnect())
activeGainNodes.forEach(gainNode => gainNode.disconnect())
activeAnalyzers.forEach(analyser => analyser.disconnect())
activeWorkletNodes.forEach(worklet => worklet.disconnect())
// Clear sets
activeSources.clear()
activeGainNodes.clear()
activeAnalyzers.clear()
activeWorkletNodes.clear()
// Close context
if (context && context.state !== 'closed') {
await context.close()
}
context = undefined
isReady = false
workletLoaded = false
error = ''
notifyListeners()
}
export function getAudioContextState(): State {
return {
isReady,
sampleRate,
state: context?.state ?? 'closed',
error,
isInitializing,
workletLoaded,
currentTime: context?.currentTime ?? 0,
state: context?.state ?? 'closed',
}
listeners.forEach((listener) => {
try {
listener(state)
}
catch (err) {
console.error('AudioContext state listener error:', err)
}
})
}
export function getAudioContext(): AudioContext | undefined {
return context
}
export function getCurrentTime(): number {
return context?.currentTime ?? 0
}
export function isAudioContextReady(): boolean {
return isReady
}
// Event subscription
export function subscribeToAudioContext(listener: (state: State) => void): () => void {
listeners.add(listener)
return () => listeners.delete(listener)
}
// Browser cleanup
@@ -7,30 +7,30 @@ import { ConverterType, create } from '@alexanderolsen/libsamplerate-js'
import { errorMessageFromValue } from '../utils/error-message'
interface ProcessorOptions {
bufferSize: number
channels: number
converterType: ConverterTypeValue
inputSampleRate: number
outputSampleRate: number
channels: number
converterType: ConverterTypeValue
bufferSize: number
}
class ResamplingAudioWorkletProcessor extends AudioWorkletProcessor {
private bufferSize: number
private converter: Awaited<ReturnType<typeof create>> | null = null
private inputBuffer: Float32Array[] = []
private isInitialized = false
private options: ProcessorOptions
private inputBuffer: Float32Array[] = []
private outputBuffer: Float32Array[] = []
private bufferSize: number
constructor(options: AudioWorkletNodeOptions) {
super()
this.options = {
bufferSize: options.processorOptions?.bufferSize || 4096,
channels: options.processorOptions?.channels || 1,
converterType: options.processorOptions?.converterType || ConverterType.SRC_SINC_MEDIUM_QUALITY,
inputSampleRate: options.processorOptions?.inputSampleRate || 44100,
outputSampleRate: options.processorOptions?.outputSampleRate || 16000,
channels: options.processorOptions?.channels || 1,
converterType: options.processorOptions?.converterType || ConverterType.SRC_SINC_MEDIUM_QUALITY,
bufferSize: options.processorOptions?.bufferSize || 4096,
}
this.bufferSize = this.options.bufferSize
@@ -51,6 +51,47 @@ class ResamplingAudioWorkletProcessor extends AudioWorkletProcessor {
}
}
private async initializeConverter() {
try {
this.converter = await create(
this.options.channels,
this.options.inputSampleRate,
this.options.outputSampleRate,
{
converterType: this.options.converterType,
},
)
this.isInitialized = true
this.port.postMessage({ type: 'initialized', success: true })
}
catch (error) {
console.error('Failed to initialize sample rate converter:', error)
this.port.postMessage({
type: 'initialized',
success: false,
error: errorMessageFromValue(error),
})
}
}
private async updateOptions(newOptions: Partial<ProcessorOptions>) {
const needsReinitialize
= newOptions.inputSampleRate !== this.options.inputSampleRate
|| newOptions.outputSampleRate !== this.options.outputSampleRate
|| newOptions.channels !== this.options.channels
|| newOptions.converterType !== this.options.converterType
Object.assign(this.options, newOptions)
if (needsReinitialize && this.converter) {
this.converter.destroy()
this.converter = null
this.isInitialized = false
await this.initializeConverter()
}
}
process(inputs: Float32Array[][], outputs: Float32Array[][]): boolean {
const input = inputs[0]
const output = outputs[0]
@@ -76,12 +117,12 @@ class ResamplingAudioWorkletProcessor extends AudioWorkletProcessor {
// Send resampled data to main thread
this.port.postMessage({
type: 'audioData',
channel,
data: resampledData,
originalSampleRate: this.options.inputSampleRate,
outputSampleRate: this.options.outputSampleRate,
timestamp: currentTime,
type: 'audioData',
})
// Copy to output (you might want to buffer this properly for different sample rates)
@@ -102,8 +143,8 @@ class ResamplingAudioWorkletProcessor extends AudioWorkletProcessor {
console.error('Resampling error in worklet:', error)
this.port.postMessage({
error: errorMessageFromValue(error),
type: 'error',
error: errorMessageFromValue(error),
})
// Pass through original data on error
@@ -116,47 +157,6 @@ class ResamplingAudioWorkletProcessor extends AudioWorkletProcessor {
return true
}
private async initializeConverter() {
try {
this.converter = await create(
this.options.channels,
this.options.inputSampleRate,
this.options.outputSampleRate,
{
converterType: this.options.converterType,
},
)
this.isInitialized = true
this.port.postMessage({ success: true, type: 'initialized' })
}
catch (error) {
console.error('Failed to initialize sample rate converter:', error)
this.port.postMessage({
error: errorMessageFromValue(error),
success: false,
type: 'initialized',
})
}
}
private async updateOptions(newOptions: Partial<ProcessorOptions>) {
const needsReinitialize
= newOptions.inputSampleRate !== this.options.inputSampleRate
|| newOptions.outputSampleRate !== this.options.outputSampleRate
|| newOptions.channels !== this.options.channels
|| newOptions.converterType !== this.options.converterType
Object.assign(this.options, newOptions)
if (needsReinitialize && this.converter) {
this.converter.destroy()
this.converter = null
this.isInitialized = false
await this.initializeConverter()
}
}
}
registerProcessor('resampling-processor', ResamplingAudioWorkletProcessor)
+70 -70
View File
@@ -1,73 +1,9 @@
import { encodeBase64 } from '@moeru/std/base64'
/**
* Converts little-endian signed PCM16 bytes to normalized Float32 PCM samples.
*
* @example
* toFloat32FromPCM16(new Uint8Array([0, 128, 0, 0, 255, 127]))
* // => Float32Array([-1, 0, 0.999969482421875])
*/
export function toFloat32FromPCM16(pcmBytes: Uint8Array): Float32Array<ArrayBuffer> {
if (pcmBytes.byteLength % Int16Array.BYTES_PER_ELEMENT !== 0)
throw new TypeError('PCM16 input must contain complete 16-bit samples.')
const dataView = new DataView(pcmBytes.buffer, pcmBytes.byteOffset, pcmBytes.byteLength)
const output = new Float32Array(pcmBytes.byteLength / Int16Array.BYTES_PER_ELEMENT)
for (let i = 0; i < output.length; i++)
output[i] = dataView.getInt16(i * Int16Array.BYTES_PER_ELEMENT, true) / 0x8000
return output
}
/**
* Converts normalized Float32 PCM samples to little-endian signed PCM16 bytes.
* Values outside the normalized range are clamped.
*
* @example
* toPCM16FromFloat32(new Float32Array([-1, 0, 1]))
* // => Uint8Array([0, 128, 0, 0, 255, 127])
*/
export function toPCM16FromFloat32(samples: Float32Array): Uint8Array<ArrayBuffer> {
const output = new Uint8Array(samples.length * Int16Array.BYTES_PER_ELEMENT)
const dataView = new DataView(output.buffer)
for (let i = 0; i < samples.length; i++) {
const sample = Math.max(-1, Math.min(1, samples[i]))
const value = sample < 0 ? sample * 0x8000 : sample * 0x7FFF
dataView.setInt16(i * Int16Array.BYTES_PER_ELEMENT, value, true)
function writeString(dataView: DataView, offset: number, string: string) {
for (let i = 0; i < string.length; i++) {
dataView.setUint8(offset + i, string.charCodeAt(i))
}
return output
}
/**
* Encodes Float32 samples as a WAV file.
*
* @example
* toWav(float32Samples.buffer, 24000)
* // => WAV data with converted PCM16 samples
*/
export function toWav(buffer: ArrayBufferLike, sampleRate: number, channel = 1): ArrayBuffer {
const samples = new Float32Array(buffer)
return toWavFromPCM16(toPCM16FromFloat32(samples), sampleRate, channel)
}
export function toWAVBase64(buffer: ArrayBufferLike, sampleRate: number) {
return encodeBase64(toWav(buffer, sampleRate))
}
/**
* Wraps raw signed 16-bit PCM samples in a WAV file.
*
* @example
* toWavFromPCM16(pcmBytes, 24000)
* // => WAV data with the original PCM16 bytes
*/
export function toWavFromPCM16(pcmBytes: Uint8Array, sampleRate: number, channel = 1): ArrayBuffer {
const arrayBuffer = createWavBuffer(pcmBytes.byteLength, sampleRate, channel)
new Uint8Array(arrayBuffer, 44).set(pcmBytes)
return arrayBuffer
}
function createWavBuffer(dataSize: number, sampleRate: number, channel: number): ArrayBuffer {
@@ -96,8 +32,72 @@ function createWavBuffer(dataSize: number, sampleRate: number, channel: number):
return arrayBuffer
}
function writeString(dataView: DataView, offset: number, string: string) {
for (let i = 0; i < string.length; i++) {
dataView.setUint8(offset + i, string.charCodeAt(i))
/**
* Converts normalized Float32 PCM samples to little-endian signed PCM16 bytes.
* Values outside the normalized range are clamped.
*
* @example
* toPCM16FromFloat32(new Float32Array([-1, 0, 1]))
* // => Uint8Array([0, 128, 0, 0, 255, 127])
*/
export function toPCM16FromFloat32(samples: Float32Array): Uint8Array<ArrayBuffer> {
const output = new Uint8Array(samples.length * Int16Array.BYTES_PER_ELEMENT)
const dataView = new DataView(output.buffer)
for (let i = 0; i < samples.length; i++) {
const sample = Math.max(-1, Math.min(1, samples[i]))
const value = sample < 0 ? sample * 0x8000 : sample * 0x7FFF
dataView.setInt16(i * Int16Array.BYTES_PER_ELEMENT, value, true)
}
return output
}
/**
* Converts little-endian signed PCM16 bytes to normalized Float32 PCM samples.
*
* @example
* toFloat32FromPCM16(new Uint8Array([0, 128, 0, 0, 255, 127]))
* // => Float32Array([-1, 0, 0.999969482421875])
*/
export function toFloat32FromPCM16(pcmBytes: Uint8Array): Float32Array<ArrayBuffer> {
if (pcmBytes.byteLength % Int16Array.BYTES_PER_ELEMENT !== 0)
throw new TypeError('PCM16 input must contain complete 16-bit samples.')
const dataView = new DataView(pcmBytes.buffer, pcmBytes.byteOffset, pcmBytes.byteLength)
const output = new Float32Array(pcmBytes.byteLength / Int16Array.BYTES_PER_ELEMENT)
for (let i = 0; i < output.length; i++)
output[i] = dataView.getInt16(i * Int16Array.BYTES_PER_ELEMENT, true) / 0x8000
return output
}
/**
* Encodes Float32 samples as a WAV file.
*
* @example
* toWav(float32Samples.buffer, 24000)
* // => WAV data with converted PCM16 samples
*/
export function toWav(buffer: ArrayBufferLike, sampleRate: number, channel = 1): ArrayBuffer {
const samples = new Float32Array(buffer)
return toWavFromPCM16(toPCM16FromFloat32(samples), sampleRate, channel)
}
/**
* Wraps raw signed 16-bit PCM samples in a WAV file.
*
* @example
* toWavFromPCM16(pcmBytes, 24000)
* // => WAV data with the original PCM16 bytes
*/
export function toWavFromPCM16(pcmBytes: Uint8Array, sampleRate: number, channel = 1): ArrayBuffer {
const arrayBuffer = createWavBuffer(pcmBytes.byteLength, sampleRate, channel)
new Uint8Array(arrayBuffer, 44).set(pcmBytes)
return arrayBuffer
}
export function toWAVBase64(buffer: ArrayBufferLike, sampleRate: number) {
return encodeBase64(toWav(buffer, sampleRate))
}
+2 -2
View File
@@ -2,14 +2,14 @@ import { defineConfig } from 'tsdown'
export default defineConfig({
entry: {
'index': 'src/index.ts',
'audio-context/index': 'src/audio-context/index.ts',
'audio-context/processor.worklet': 'src/audio-context/processor.worklet.ts',
'encoding/index': 'src/encoding/index.ts',
'index': 'src/index.ts',
},
unbundle: true,
external: [
'@alexanderolsen/libsamplerate-js/dist/libsamplerate.worklet.js?worker&url',
'./processor.worklet?worker&url',
],
unbundle: true,
})