Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
const BUTTON_NAMES = ['a', 'b', 'c']
const MOD_INSTALL_HOOKS = ['_fxMainSetModArchive', '_wasmModInstallArchive']
export function createHostButtonBridge({
logger = console.log,
setTimeoutFn = globalThis.setTimeout,
resetDelayMs = 120,
} = {}) {
const states = Object.fromEntries(
BUTTON_NAMES.map((name) => [name, { pressed: 1, firmwareCallbacks: new Set() }]),
)
const Button = Object.fromEntries(
BUTTON_NAMES.map((name) => [
name,
class HtmlBridgeButton {
constructor({ onPush } = {}) {
if (onPush) states[name].firmwareCallbacks.add(onPush)
}
read() {
return states[name].pressed
}
},
])
)
return {
Button,
push(name) {
const state = states[name]
if (!state) return
logger(`[bridge] Host.Button.${name} pushed`)
state.pressed = 0
for (const callback of state.firmwareCallbacks) callback()
setTimeoutFn(() => {
state.pressed = 1
}, resetDelayMs)
},
read(name) {
return states[name]?.pressed
},
}
}
export function createHostDriverBridge({ onRotation = () => {}, onTorque = () => {} } = {}) {
let rotation = { y: 0, p: 0, r: 0 }
let torque = true
return {
applyRotation(message = {}) {
rotation = { ...rotation, ...(message.rotation ?? {}) }
onRotation(rotation, message.time)
},
getRotation() {
return rotation
},
setTorque(nextTorque) {
torque = Boolean(nextTorque)
onTorque(torque)
},
getTorque() {
return torque
},
}
}
export function installModArchiveIntoWasm(wasmModule, installedMod) {
if (!installedMod) return { status: 'empty' }
const bytes = installedMod.bytes instanceof Uint8Array ? installedMod.bytes : new Uint8Array(installedMod.bytes ?? [])
const size = installedMod.size ?? bytes.byteLength
const hookName = MOD_INSTALL_HOOKS.find((name) => typeof wasmModule?.[name] === 'function')
if (typeof wasmModule?._malloc !== 'function' || !wasmModule.HEAPU8) {
return { status: 'unsupported', name: installedMod.name, size }
}
const pointer = wasmModule._malloc(bytes.byteLength)
wasmModule.HEAPU8.set(bytes, pointer)
if (!hookName) return { status: 'prepared', pointer, name: installedMod.name, size }
try {
const result = wasmModule[hookName](pointer, bytes.byteLength)
return { status: 'installed', hook: hookName, name: installedMod.name, size, result }
} finally {
wasmModule._free?.(pointer)
}
}
export function createHostAudioOutBridge({
createAudioContext = defaultAudioContextFactory,
setTimeoutFn = globalThis.setTimeout,
clearTimeoutFn = globalThis.clearTimeout,
} = {}) {
let context
return {
async tone({ hz = 440, duration = 100, volume = 1 } = {}) {
context ??= createAudioContext()
if (context.state === 'suspended' && typeof context.resume === 'function') {
await context.resume()
}
const oscillator = context.createOscillator()
const gain = context.createGain()
oscillator.frequency.value = hz
gain.gain.value = volume
oscillator.connect(gain)
gain.connect(context.destination)
const startTime = context.currentTime
await new Promise((resolve, reject) => {
let fallback
const finish = () => {
if (fallback !== undefined) clearTimeoutFn?.(fallback)
resolve()
}
oscillator.onended = finish
try {
oscillator.start(startTime)
oscillator.stop(startTime + duration / 1000)
fallback = setTimeoutFn?.(finish, duration + 250)
} catch (error) {
if (fallback !== undefined) clearTimeoutFn?.(fallback)
reject(error)
}
})
},
async play(buffer) {
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength === 0) return false
context ??= createAudioContext()
if (context.state === 'suspended' && typeof context.resume === 'function') {
await context.resume()
}
if (typeof context.decodeAudioData !== 'function') return false
const audioBuffer = await decodeAudioData(context, buffer)
const source = context.createBufferSource()
source.buffer = audioBuffer
source.connect(context.destination)
await new Promise((resolve, reject) => {
let fallback
const durationMilliSec = Number.isFinite(audioBuffer.duration) ? audioBuffer.duration * 1000 : 0
const finish = () => {
if (fallback !== undefined) clearTimeoutFn?.(fallback)
resolve()
}
source.onended = finish
try {
source.start(0)
if (durationMilliSec > 0) {
fallback = setTimeoutFn?.(finish, durationMilliSec + 250)
}
} catch (error) {
if (fallback !== undefined) clearTimeoutFn?.(fallback)
reject(error)
}
})
return true
},
close() {
context?.close?.()
context = undefined
},
}
}
function decodeAudioData(context, buffer) {
return new Promise((resolve, reject) => {
const result = context.decodeAudioData(buffer.slice(0), resolve, reject)
if (result && typeof result.then === 'function') {
result.then(resolve, reject)
}
})
}
export function createHostAudioInBridge({
mediaDevices = globalThis.navigator?.mediaDevices,
MediaRecorder = globalThis.MediaRecorder,
setTimeoutFn = globalThis.setTimeout,
} = {}) {
return {
async record(durationMilliSec = 3000) {
if (!mediaDevices?.getUserMedia || !MediaRecorder) return new ArrayBuffer(0)
const format = selectAudioRecordingFormat(MediaRecorder)
if (!format) {
return new ArrayBuffer(0)
}
const stream = await mediaDevices.getUserMedia({ audio: true })
const chunks = []
try {
return await new Promise((resolve) => {
const recorder = new MediaRecorder(stream, { mimeType: format.mimeType })
recorder.ondataavailable = (event) => {
if (event.data) chunks.push(event.data)
}
recorder.onstop = async () => {
const buffer = await chunksToArrayBuffer(chunks)
resolve(attachAudioMetadata(isSupportedAudioBuffer(buffer, format) ? buffer : new ArrayBuffer(0), format))
}
recorder.start()
setTimeoutFn(() => recorder.stop(), durationMilliSec)
})
} finally {
for (const track of stream.getTracks?.() ?? []) track.stop?.()
}
},
}
}
const AUDIO_RECORDING_FORMATS = Object.freeze([
{ mimeType: 'audio/webm;codecs=opus', extension: 'webm' },
{ mimeType: 'audio/webm', extension: 'webm' },
{ mimeType: 'audio/mp4', extension: 'm4a' },
{ mimeType: 'audio/wav', extension: 'wav' },
])
function selectAudioRecordingFormat(MediaRecorder) {
if (typeof MediaRecorder.isTypeSupported !== 'function') return AUDIO_RECORDING_FORMATS[0]
return AUDIO_RECORDING_FORMATS.find(({ mimeType }) => MediaRecorder.isTypeSupported(mimeType))
}
function isSupportedAudioBuffer(buffer, format) {
if (format.mimeType === 'audio/wav') return isWavBuffer(buffer)
return buffer instanceof ArrayBuffer && buffer.byteLength > 0
}
function attachAudioMetadata(buffer, format) {
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength === 0) return buffer
const metadata = {
mimeType: format.mimeType,
filename: `speak.${format.extension}`,
}
try {
Object.defineProperties(buffer, {
mimeType: { value: metadata.mimeType, configurable: true },
filename: { value: metadata.filename, configurable: true },
})
} catch {
buffer.mimeType = metadata.mimeType
buffer.filename = metadata.filename
}
return buffer
}
function defaultAudioContextFactory() {
const AudioContextConstructor = globalThis.AudioContext ?? globalThis.webkitAudioContext
if (!AudioContextConstructor) throw new Error('WebAudio AudioContext is not available')
return new AudioContextConstructor()
}
async function chunksToArrayBuffer(chunks) {
const buffers = await Promise.all(
chunks.map(async (chunk) => {
if (chunk instanceof ArrayBuffer) return chunk
if (ArrayBuffer.isView(chunk)) return chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength)
if (typeof chunk.arrayBuffer === 'function') return chunk.arrayBuffer()
return new ArrayBuffer(0)
}),
)
const total = buffers.reduce((sum, buffer) => sum + buffer.byteLength, 0)
const bytes = new Uint8Array(total)
let offset = 0
for (const buffer of buffers) {
bytes.set(new Uint8Array(buffer), offset)
offset += buffer.byteLength
}
return bytes.buffer
}
function isWavBuffer(buffer) {
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength < 12) return false
const bytes = new Uint8Array(buffer)
return (
bytes[0] === 0x52 &&
bytes[1] === 0x49 &&
bytes[2] === 0x46 &&
bytes[3] === 0x46 &&
bytes[8] === 0x57 &&
bytes[9] === 0x41 &&
bytes[10] === 0x56 &&
bytes[11] === 0x45
)
}
export function clientPointFromTouch(touch) {
return { x: touch.clientX, y: touch.clientY }
}
export function summarizeImageData(imageData, { sampleLimit = 1024 } = {}) {
const data = imageData?.data ?? imageData
if (!data?.length) return { samples: 0, nonZeroAlpha: 0, nonZeroRgb: 0, firstPixel: [] }
const pixels = Math.floor(data.length / 4)
const stride = Math.max(1, Math.floor(pixels / sampleLimit))
let samples = 0
let nonZeroAlpha = 0
let nonZeroRgb = 0
for (let pixel = 0; pixel < pixels; pixel += stride) {
const offset = pixel * 4
samples++
if (data[offset + 3] !== 0) nonZeroAlpha++
if (data[offset] !== 0 || data[offset + 1] !== 0 || data[offset + 2] !== 0) nonZeroRgb++
}
return {
samples,
nonZeroAlpha,
nonZeroRgb,
firstPixel: Array.from(data.slice(0, 4)),
}
}
47ea1b423This file is pinned to the selected release. Download the original to open it in your design tool.