bass.streamPlayer 接收 MP3 等压缩音频的连续文件字节。它不需要完整文件、不依赖 TTS、HTTP 或 WebSocket,可以用于任意能够分块交付音频的生产者。
import bass.streamPlayer;
var player = bass.streamPlayer();
// 收到生产者的一个编码音频块时:
// var ok,err = player.write(data);
// 生产者确认不会再发送任何数据时:
// player.finish();
// 生产者失败时(不把残缺音频当作正常结束):
// player.finish("错误信息");
// 用户打断或窗口关闭时:
player.close();
BASS 是闭源的第三方组件:非商业用途免费,商业用途需按 un4seen 官网 购买授权。只需播放 MP3(例如 TTS 语音)时,可改用免费的系统原生播放器 sys.mediaFoundation.streamingMp3Player,接口相同。
所有操作在创建播放器的线程执行,线程需要消息循环。wait(timeoutMs) 是带消息泵的便利方法;GUI 可以使用已有的 win.loopMessage()。
一个实例代表一个连续编码文件流,不是多个独立 MP3 文件的播放列表。它不是 PCM 样本推送器。
| 方法/属性 | 语义 |
|---|---|
write(data) |
接收字符串/buffer 字节;成功 true,失败 null,错误字符串;可重用传入的 buffer |
finish() |
结束输入,先送完排队数据,再通知解码器 EOF;不会立即停止声音;可重复调用 |
finish(err) |
来源失败:立即停止并触发一次 onError;err 可以是错误字符串或含 message 的错误表 |
play() |
开始或继续播放;可在尚无音频时调用 |
pause() |
暂停播放,仍可接收输入,但仍受缓存上限限制 |
close() |
立即停止并释放当前实例,可重复调用,不触发 onEnd |
wait(timeoutMs) |
等待实际播放完毕;超时不自动停止,需要时显式 close |
state |
buffering、playing、paused、ended、closed、failed |
done |
已播完、关闭或失败 |
channel |
格式识别后可访问 bass.channel;结束后为 null。不要自行 free |
bufferedBytes |
aardio 层待送入 BASS 的编码字节数,不包括 BASS 自己的缓冲 |
receivedBytes |
已收到的编码字节总数 |
error |
失败信息 |
事件可在构造参数中给出,也可在第一次 write 之前设置:
onStart():首次成功启动 BASS 播放请求,不是声卡输出首个样本的精确时钟。onEnd():输入结束,缓冲已播完,原生资源已释放。onError(err):失败,资源已释放。回调自身抛错记入 callbackError。空块 write("") 是无操作,不是 EOF。生产者错误时调用 finish(err) 或 close(),不要把不完整的音频当作正常完成。
import bass.streamPlayer;
var player = bass.streamPlayer({
initialBytes=2048; // 初次尝试识别格式的字节数
maxInitialBytes=65536; // 初始头部识别预算
maxBufferBytes=4194304; // aardio 层待播队列上限,4 MiB
volume=1; // 本地播放音量 0..1
autoPlay=true;
});
player.close();
初始数据不足以识别格式时,在预算内积累更多字节重试;短音频调用 finish 后会立即尝试识别。无法识别、缓冲超限或原生调用失败都明确返回错误,不静默丢块。
当 BASS 只能接收一部分输入时,剩余数据由播放器保留并通过短定时器继续提交。EOF 只在剩余数据全部接收后发送。每个流单独设置低延迟恢复与播放缓冲,不修改进程全局 BASS 缓冲设置。
内置解码器能处理的格式不等于所有文件都能从流中解码:依赖文件尾索引、过大的前置标签或额外插件的格式可能失败。此次重点验证的是 TTS 返回的 MP3。可通过提高初始识别预算处理合理的大头部,但不要取消内存上限。
只把确认是音频的字节写入播放器。 写入 BASS 无法识别的数据(例如 HTTP 错误页、文本、格式错误的文件)时,BASS 会调用系统的 Media Foundation 继续探测格式。本机实测(32 位进程):推送流上的这一次探测耗时约 3 秒,最后返回 BASS_ERROR_FILEFORM(41),却占用约 1.9GB 进程地址空间且不再释放。32 位进程随后无法创建线程,出现“内存不够”、“创建线程时遇到错误”等与音频无关的故障。
同样的数据改用内存流 BASS_StreamCreateFile,或先禁用 BASS 的 Media Foundation,都不会出现。这是闭源 BASS 内部的问题,本库没有做规避。需要处理不可信数据时:
Content-Type;::Bass.BASS_SetConfig(40/*BASS_CONFIG_MF_DISABLE*/,1) 禁用 Media Foundation。这是进程全局设置,会影响之后创建的所有 BASS 流;播放 MP3/OGG 不需要它,但依赖系统解码器的 AAC 等格式会因此无法播放;sys.mediaFoundation.streamingMp3Player。它直接调用系统 MP3 解码器,不做格式探测;整段输入都不是 MP3 时,在 finish() 报告“未解码出音频”。bass.channel.create(...).push(...):输入是已解码的 PCM 样本,不能拿来直接推送 MP3。bass.channel.openPush(initialData,flags) + channel.pushFile(...):新增加的底层压缩文件推送接口。bass.streamPlayer:推荐入口,管理初始数据、部分写入、缓存、播放与 EOF。openPush 成功返回 channel,consumedBytes。BASS 不一定消费所有初始数据;必须先补送未消费的初始部分,再送新到达的数据。失败返回 null,错误字符串,BASS错误码。
pushFile(data,length) 返回实际接收的字节数。返回 0 表示暂时没有空间,不代表失败;未接收字节必须保留。pushFile(null,0) 是 EOF,不是“空块”。
openPush 禁止 BASS_STREAM_AUTOFREE。必须在创建线程显式 free(),以保持初始原生回调的生命周期;不要在其他线程释放回调。高级播放器已经正确处理这些细节。
播放器有 write/finish 方法,可以直接作为 web.edgeTextToSpeech 的输出对象:
import web.edgeTextToSpeech;
import bass.streamPlayer;
var player = bass.streamPlayer();
var task,err = web.edgeTextToSpeech().synthesize({text="收到前面的音频就开始播放。"}, player);
if(!task){player.close();return print(err.message);}
var ok,err = player.wait(120000); // 合成失败也会传给播放器
if(!ok) print(err);
task.cancel();
player.close();
测试包括任意输入切分、原生部分写入、EOF、取消、空输入、缓存溢出,以及在只收到前缀时实际播放位置前进。不同输入切分在相同解码模式下生成完全一致的 PCM 数据。