Example PRO

import { AudioUnitView, Button, List, Navigation, NavigationStack, Script, Text, useState } from "scripting"

let engine: AVAudioEngine | null = null
let unit: AVAudioUnit | null = null
let player: AVAudioPlayerNode | null = null
let buffer: AVAudioPCMBuffer | null = null
let editor: AVAudioUnitViewController | null = null
let observer: AVAudio.ParameterObserver | null = null
let pending: Promise<void> | null = null
let polling: Promise<void> | null = null
let pollActive = false
let alive = true
let savedState: Uint8Array | null = null
function flatten(node: AVAudio.ParameterGroup | AVAudio.Parameter): AVAudio.Parameter[] {
  return "address" in node ? [node] : node.children.flatMap(flatten)
}
async function release() {
  pollActive = false; await polling; polling = null
  await observer?.close(); observer = null; await editor?.dispose(); editor = null
  await engine?.stop(); await engine?.dispose(); engine = null
  await player?.dispose(); player = null; await unit?.dispose(); unit = null
  await buffer?.dispose(); buffer = null; savedState = null
  await SharedAudioSession.setActive(false)
}
function Demo() {
  const dismiss = Navigation.useDismiss()
  const [message, setMessage] = useState("Find an effect or instrument installed on this device.")
  const [components, setComponents] = useState<readonly AVAudio.Component[]>([])
  const [parameters, setParameters] = useState<readonly AVAudio.Parameter[]>([])
  const [values, setValues] = useState<Readonly<Record<string, number>>>({})
  const [presets, setPresets] = useState<readonly AVAudio.Preset[]>([])
  const [controller, setController] = useState<AVAudioUnitViewController | null>(null)
  const [busy, setBusy] = useState(false)
  const [ready, setReady] = useState(false)
  const [instrument, setInstrument] = useState(false)
  const report = (text: string) => { if (alive) setMessage(text) }
  async function refresh() {
    if (!unit) return
    const entries = await unit.getParameterValues(parameters.map(parameter => parameter.address))
    if (alive) setValues(Object.fromEntries(entries.map(entry => [entry.address, entry.value])))
  }
  async function execute(action: () => Promise<void>) {
    if (pending) return
    setBusy(true)
    pending = action().catch(error => report(`${(error as AVAudio.Error).code ?? "error"}: ${String(error)}`)).finally(() => { pending = null; if (alive) setBusy(false) })
    await pending
  }
  async function find(type: number) {
    const entries = await AVAudioUnit.findComponents({ type })
    setComponents(entries); report(`${entries.length} registered components. Tap a name to load it. Loading can still fail.`)
  }
  async function load(component: AVAudio.Component) {
    setController(null); setReady(false); await release(); setParameters([]); setValues({}); setPresets([])
    const description = { type: component.type, subtype: component.subtype, manufacturer: component.manufacturer }
    try {
      const isInstrument = component.type === 0x61756d75
      unit = isInstrument ? await AVAudioUnitMIDIInstrument.create(description) : await AVAudioUnit.instantiate(description)
      setInstrument(isInstrument)
      const tree = await unit.getParameterTree(), metadata = tree ? flatten(tree) : []
      // A concise native-control alternative remains usable without a plugin editor.
      const visible = metadata.slice(0, 8); setParameters(visible)
      const current = await unit.getParameterValues(visible.map(parameter => parameter.address)); setValues(Object.fromEntries(current.map(entry => [entry.address, entry.value])))
      setPresets((await unit.getFactoryPresets()).slice(0, 12))
      const buses = await unit.getBusState()
      await SharedAudioSession.setCategory("playback", []); await SharedAudioSession.setActive(true)
      engine = await AVAudioEngine.create(); await engine.attach(unit)
      const format = new AVAudioFormat({ sampleRate: 48000, channelCount: 2 })
      await engine.connect(unit, await engine.getMainMixerNode(), { format })
      if (!isInstrument) {
        player = await AVAudioPlayerNode.create(); await engine.attach(player); await engine.connect(player, unit, { format })
        const samples = new Float32Array(48000)
        for (let frame = 0; frame < samples.length; frame++) samples[frame] = Math.sin(2 * Math.PI * 440 * frame / 48000) * 0.1 * Math.exp(-(frame % 12000) / 2500)
        buffer = await AVAudioPCMBuffer.fromChannels([samples, samples], format); await player.scheduleBuffer(buffer, { loops: true }); await player.setMixing({ volume: 0.3 })
      }
      await engine.start(); setReady(true)
      editor = await unit.requestView(); setController(editor)
      report(`${component.name}\n${buses.inputs.length} inputs / ${buses.outputs.length} outputs · latency ${buses.latency}s\n${editor ? "Native editor below." : "No native editor. Use the parameter controls."}`)
      if (tree) {
        observer = await unit.observeParameters({ queueSize: 128 }); const activeObserver = observer, activeUnit = unit
        pollActive = true
        polling = (async () => {
          while (pollActive && alive) {
            const changes = await activeObserver.next()
            if (changes?.length) {
              const entries = await activeUnit.getParameterValues(visible.map(parameter => parameter.address))
              if (alive) setValues(Object.fromEntries(entries.map(entry => [entry.address, entry.value])))
            }
            await new Promise<void>(resolve => setTimeout(resolve, 200))
          }
        })().catch(error => { pollActive = false; report(`Parameter observation stopped: ${String(error)}. Reload the component if its tree changed.`) })
      }
    } catch (error) { setController(null); setReady(false); await release(); throw error }
  }
  async function setParameter(parameter: AVAudio.Parameter, fraction: number) {
    await unit?.setParameterValues([{ address: parameter.address, value: parameter.minimum + (parameter.maximum - parameter.minimum) * fraction }]); await refresh()
  }
  return <NavigationStack><List navigationTitle="Audio Unit Host" toolbar={{ cancellationAction: <Button title="Close" action={dismiss} /> }}>
    <Text>{message}</Text>
    <Button title="Find effects" action={() => { void execute(() => find(0x61756678)) }} disabled={busy} />
    <Button title="Find instruments" action={() => { void execute(() => find(0x61756d75)) }} disabled={busy} />
    {components.map(component => <Button key={`${component.type}-${component.subtype}-${component.manufacturer}`} title={`Load ${component.name}`} action={() => { void execute(() => load(component)) }} disabled={busy} />)}
    <Button title={instrument ? "Play instrument note 60" : "Play generated tone through effect"} action={() => { void execute(async () => { if (instrument) await (unit as AVAudioUnitMIDIInstrument).startNote(60, 80); else await player?.play() }) }} disabled={!ready || busy} />
    <Button title="Stop notes or pause tone" action={() => { void execute(async () => { if (instrument) await (unit as AVAudioUnitMIDIInstrument).allNotesOff(); else await player?.pause() }) }} disabled={!ready || busy} />
    <Button title="Save state snapshot in memory" action={() => { void execute(async () => { if (unit) savedState = await unit.getStateData({ forDocument: true }); report("State snapshot saved until this component is unloaded") }) }} disabled={!ready || busy} />
    <Button title="Restore saved state" action={() => { void execute(async () => { if (unit && savedState) { pollActive = false; await polling; polling = null; await observer?.close(); observer = null; await unit.setStateData(savedState, { forDocument: true }); await refresh(); report("Restored. Reload if the plugin changed its parameter tree.") } else report("Save a state snapshot first") }) }} disabled={!ready || busy} />
    {presets.map(preset => <Button key={preset.number} title={`Factory preset: ${preset.name}`} action={() => { void execute(async () => { await unit?.setCurrentPreset(preset); await refresh(); report(`Selected preset ${preset.name}. Plugin changes may arrive asynchronously.`) }) }} disabled={!ready || busy} />)}
    {parameters.map(parameter => <><Text key={`${parameter.address}-value`}>{`${parameter.name}: ${values[parameter.address] ?? "—"} · ${parameter.address}`}</Text>
      <Button key={`${parameter.address}-min`} title={`Set ${parameter.name} to minimum`} action={() => { void execute(() => setParameter(parameter, 0)) }} disabled={!ready || busy || !(parameter.flags & 0x80000000)} />
      <Button key={`${parameter.address}-mid`} title={`Set ${parameter.name} to midpoint`} action={() => { void execute(() => setParameter(parameter, 0.5)) }} disabled={!ready || busy || !(parameter.flags & 0x80000000)} />
      <Button key={`${parameter.address}-max`} title={`Set ${parameter.name} to maximum`} action={() => { void execute(() => setParameter(parameter, 1)) }} disabled={!ready || busy || !(parameter.flags & 0x80000000)} />
    </>)}
    {controller ? <AudioUnitView controller={controller} frame={{ minHeight: 280 }} /> : null}
    <Button title="Unload component and release audio" action={() => { void execute(async () => { setController(null); setReady(false); await release(); setParameters([]); report("Unloaded") }) }} disabled={busy} />
  </List></NavigationStack>
}
async function main() { try { await Navigation.present({ element: <Demo /> }) } finally { try { alive = false; await engine?.stop(); await pending; await release(); } finally { Script.exit() } } }
void main()