playback
playback 把 guest 生成的 PCM 采样送到系统音频输出。宿主代持音频设备和有界缓冲;guest 不获得 Web Audio context、设备对象或任意音频图。
适用场景
- 游戏或模拟器实时生成声音。
- 语音合成、音乐工具或解码器播放自己产生的 PCM。
- 需要可观察背压的连续音频流。
播放包内压缩音频时,guest 先用 L2 codec 解码;多轨混音、效果器、空间音频和音乐时间线也属于 guest 库。
L1 边界
宿主独占的是系统音频设备、发声许可和受预算的输出缓冲。guest 无法仅靠内存产生系统声音;编解码、混音、合成和播放列表都能在 guest 中生成 PCM,因此停留在 L2/L3。
能力声明
{
"capabilities": ["host:playback.open"],
"events": ["playback.ready", "playback.ended"]
}
write 和 stop 由 open 返回句柄中的 write 权限授权,不是可单独购买的 capability。
Reference
数据类型
PlaybackOptions {
preferredSampleRate?: u32
channels?: 1 | 2
latency?: "interactive" | "balanced"
}
PlaybackStream {
output: Handle<audio-output, write+close, app-context, session>
sampleRate: u32
channels: 1 | 2
maxQueuedFrames: u32
}
省略 channels 时默认 2,返回值必须与请求或默认声道数相同;宿主需要时在设备侧混音,不能让 guest 猜测。preferredSampleRate 是偏好,返回值可以不同;latency 省略时为 "balanced",它只选择宿主预设,不改变格式或预算语义。maxQueuedFrames 恰好等于宿主为 v0.1 公布的 playback.maxQueuedFrames。
v0.1 的采样格式固定为交错、little-endian f32 PCM。每个值必须为有限数且位于 [-1, 1];越界值、NaN 和 Infinity 都以 invalid-format 拒绝,宿主不得静默钳制。
方法
playback.open
playback.open(options?) -> PlaybackStream
创建一次会话级输出并返回宿主实际采用的采样率、声道数和最大排队帧数。采样率必须是 [8000, 192000] 内的整数;偏好不是强制要求,guest 必须按返回值生产或重采样。
playback.write
playback.write(output, samples: ByteSource) -> {
acceptedFrames: u32,
queuedFrames: u32
}
提交完整采样帧。字节数必须是 channels × 4 的整数倍。acceptedFrames 可以小于输入帧数;未接受尾部仍归 guest 所有。已经接受的帧只能由显式 stop(..., "immediate")、设备故障或系统撤销丢弃,宿主不得在正常预算压力下静默丢帧。
输入必须含 1..playback.maxWriteFrames 个完整帧。调用在线性化点观察 queuedBefore,并返回 acceptedFrames = min(inputFrames, maxQueuedFrames - queuedBefore) 与 queuedFrames = queuedBefore + acceptedFrames;缓冲已满时成功返回 0。对同一句柄的并发写入按宿主接受调用的顺序线性化,不允许用任意部分接受制造额外差异。
playback.stop
playback.stop(output, mode? = "drain") -> void
drain 立即把输出转为不可写的 draining 状态,方法返回后继续播放已接受数据,排空时关闭;immediate 丢弃缓冲并立即关闭。draining 期间重复 drain 是 no-op,改用 immediate 会立即终止;终止后任一重复停止都是 no-op。方法只等待状态转换,不等待音频排空。
事件
event playback.ready {
output,
writableFrames: u32
}
event playback.ended {
output,
reason: "stopped" | "device-lost" | "permission-revoked" | "background-policy" | "error"
}
ready 是继续写入的提示,可以合并;guest 仍必须以 write 返回值为准。
JS 工作流示例
下面按宿主实际采样率生成 440 Hz 正弦波:
const stream = await host.playback.open({
preferredSampleRate: 48_000,
channels: 1,
latency: 'interactive',
});
let phase = 0;
host.events.on('playback.ready', async ({ output, writableFrames }) => {
if (output !== stream.output) return;
const frames = Math.min(writableFrames, 1024);
const pcm = new Float32Array(frames);
for (let i = 0; i < frames; i++) {
pcm[i] = Math.sin(phase);
phase += (2 * Math.PI * 440) / stream.sampleRate;
}
await host.playback.write(stream.output, new Uint8Array(pcm.buffer));
});
用户停止时应显式选择是否排空:
await host.playback.stop(stream.output, 'immediate');
原生 WASM 绑定示例
PlaybackStream stream = host_playback_open((PlaybackOptions){48000, 2, INTERACTIVE});
float pcm[1024 * 2];
mix_stereo(pcm, 1024, stream.sample_rate);
PlaybackWriteResult r = host_playback_write(
stream.output,
byte_source((uint8_t *)pcm, sizeof(pcm))
);
if (r.accepted_frames < 1024) retain_unwritten_tail(r.accepted_frames);
guest 应保留未接受的尾部,等待下一次 ready,而不是忙循环重试。
生命周期与用户激活
- 输出句柄属于当前应用会话,不能持久化。
- 每次
open都必须继承可信用户操作;缺少激活返回activation-required。一次手势只授权当前调用,不建立后台开流授权。 - 页面隐藏时宿主终止输出、使句柄失效;若已声明
playback.ended,发送 reason 为background-policy的事件。v0.1 不提供后台音频。 - 设备断开后句柄失效;若已声明
playback.ended,发送对应终止事件。
错误
| code | 含义 | retryable |
|---|---|---|
activation-required |
平台要求用户操作后才能开始播放 | true |
invalid-format |
字节数、采样值或声道布局非法 | false |
stale-handle |
输出已关闭或属于其他会话 | false |
device-unavailable |
当前没有可用输出设备 | true |
limit-exceeded |
输出数、排队帧或写入速率超预算 | true |
安全与预算
- 信任档位:yellow,因为它产生共享硬件副作用。
- v0.1 预算键为
playback.maxStreams(每应用活动输出数)、playback.maxQueuedFrames(每个输出的排队帧)、playback.maxWriteFrames(单次调用输入帧)和playback.writesPerSecond(每应用写调用);帧指每声道各一个采样的时间步。 - 宿主不得自动选择或泄露具体设备标识。
- 页面不可见、系统静音和用户撤销必须由宿主优先执行。
- 错误数据不能进入底层音频后端。
不属于本 API
编解码、混音、音频节点图、播放列表、媒体元数据和录音不属于 playback。麦克风输入见 capture。
一致性测试
宿主至少覆盖:采样率协商、单/双声道、部分接受、缓冲溢出、NaN/Infinity、隐藏页面、设备断开、两种 stop 模式、重复停止和跨会话句柄。