Files
ArtPlayer/test/helpers/asr-audio.js
T

185 lines
4.9 KiB
JavaScript

import assert from 'node:assert/strict'
import vm from 'node:vm'
import { asrEnvironment } from './asr.js'
export function deferredAudio() {
let resolve
let reject
const promise = new Promise((yes, no) => {
resolve = yes
reject = no
})
return { promise, resolve, reject }
}
export function asrAudioEnvironment(implementation, option = {}, capabilities = {}) {
const environment = asrEnvironment(implementation)
const intervals = new Map()
const contexts = []
const recorders = []
const nodes = []
const modules = []
const blobs = new Map()
const revoked = []
const warnings = []
const errors = []
const tracks = [{
stops: 0,
stop() { this.stops++ },
}]
const stream = { getTracks: () => tracks }
const attachedElements = new WeakSet()
let nextTimer = 0
let nextUrl = 0
let captures = 0
function node(kind, value = {}) {
Object.assign(value, {
kind,
connections: [],
disconnects: 0,
connect(target) { this.connections.push(target) },
disconnect() {
this.disconnects++
this.connections = []
},
})
nodes.push(value)
return value
}
class ControlledAudioContext {
constructor(options) {
this.options = options
this.state = capabilities.suspended ? 'suspended' : 'running'
this.destination = node('destination')
this.resumes = 0
this.closes = 0
this.audioWorklet = {
addModule: async (url) => {
const blob = blobs.get(url)
assert.ok(blob, 'The actual plugin worklet must have an object URL')
modules.push({ url, blob })
capabilities.moduleEntered?.resolve()
if (capabilities.moduleGate)
await capabilities.moduleGate.promise
if (capabilities.moduleError)
throw capabilities.moduleError
},
}
contexts.push(this)
}
async resume() {
this.resumes++
this.state = 'running'
}
async close() {
this.closes++
this.state = 'closed'
}
createMediaElementSource(video) {
assert.equal(video, environment.art.video)
if (capabilities.directError)
throw capabilities.directError
if (attachedElements.has(video))
throw new Error('HTMLMediaElement already connected to a MediaElementSourceNode')
attachedElements.add(video)
return node('element')
}
createMediaStreamSource(value) {
assert.equal(value, stream)
return node('stream')
}
createGain() {
return Object.assign(node('gain'), { gain: { value: 0 } })
}
}
class ControlledAudioWorkletNode {
constructor(context, name) {
assert.ok(contexts.includes(context))
assert.equal(context.state, 'running')
assert.equal(name, 'recorder-processor')
node('recorder', this)
this.port = { onmessage: null }
recorders.push(this)
}
}
Object.assign(environment.context, {
Blob,
URL: {
createObjectURL(blob) {
const url = `blob:controlled-asr-${++nextUrl}`
blobs.set(url, blob)
return url
},
revokeObjectURL(url) {
assert.ok(blobs.has(url))
revoked.push(url)
blobs.delete(url)
},
},
AudioWorkletNode: ControlledAudioWorkletNode,
console: { warn: (...args) => warnings.push(args), error: (...args) => errors.push(args) },
setInterval(callback, delay) {
intervals.set(++nextTimer, { callback, delay })
return nextTimer
},
clearInterval(timer) { intervals.delete(timer) },
})
environment.context[capabilities.webkit ? 'webkitAudioContext' : 'AudioContext'] = ControlledAudioContext
if (capabilities.capture !== false) {
environment.art.video[capabilities.moz ? 'mozCaptureStream' : 'captureStream'] = () => {
captures++
return stream
}
}
const plugin = environment.factory(option)(environment.art)
return {
...environment,
plugin,
intervals,
contexts,
recorders,
nodes,
modules,
blobs,
revoked,
tracks,
warnings,
errors,
captures: () => captures,
send(samples, recorder = recorders.at(-1)) {
assert.equal(typeof recorder?.port.onmessage, 'function')
recorder.port.onmessage({ data: Float32Array.from(samples) })
},
tick(timer = [...intervals.keys()][0]) {
assert.ok(intervals.has(timer), 'No live capture interval')
return intervals.get(timer).callback()
},
}
}
export async function runAsrWorklet(blob) {
const messages = []
let Processor
const context = vm.createContext({
AudioWorkletProcessor: class {
port = { postMessage: value => messages.push(value) }
},
registerProcessor(name, implementation) {
assert.equal(name, 'recorder-processor')
Processor = implementation
},
})
vm.runInContext(await blob.text(), context, { timeout: 1000 })
assert.equal(typeof Processor, 'function')
return { processor: new Processor(), messages }
}