# sys.mediaFoundation.streamingMp3Player

Windows 原生流式 MP3 播放器，边收边播：**MP3 字节 → [sys.mediaFoundation.mp3Decoder](https://www.aardio.com/zh-cn/docs/library/sys/mediaFoundation/mp3Decoder/) → 16 位 PCM → [sys.waveOut](https://www.aardio.com/zh-cn/docs/library/sys/waveOut/)**。

音频数据在内存中处理，不写临时文件，不依赖 BASS、浏览器、OCX 或音频 HTTP 服务。可以直接接收 TTS 的音频，也可以接收应用从网络取得的 MP3 字节。需要自己处理 PCM 时，直接用它的两个组成模块。

## 最小 Edge TTS 示例

```aardio
import sys.mediaFoundation.streamingMp3Player;
import web.edgeTextToSpeech;

var player,err = sys.mediaFoundation.streamingMp3Player();
if(!player) return print(err);

var engine = web.edgeTextToSpeech({voice="zh-CN-XiaoxiaoNeural"});
// 播放器直接作为输出对象：音频交给 write，成功调用 finish()，失败调用 finish(错误信息)
var task,err = engine.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();
```

带输入框、播放状态及停止按钮的完整示例：`~/examples/Media/Audio/edgeTTS-mediaFoundation.aardio`。界面程序的写法：

```aardio
import win.ui;
import web.edgeTextToSpeech;
import sys.mediaFoundation.streamingMp3Player;
var winform = win.form(text="朗读");
var engine = web.edgeTextToSpeech();
var task,player;

// 唯一的清理入口：停止按钮、关闭窗口、播放结束、出错都调用它，可以重复调用
var stop = function(){
    if(task) task.cancel();
    if(player) player.close();
    task,player = null,null;
}

var speak = function(text){
    stop();
    var err;
    player,err = sys.mediaFoundation.streamingMp3Player({
        onEnd = function(){ stop(); };
        onError = function(err){ stop(); winform.msgboxErr(err); }; // 合成失败也从这里报告
    });
    if(!player) return winform.msgboxErr(err);
    task,err = engine.synthesize({text=text},player);
    if(!task){ stop(); winform.msgboxErr(err.message); }
}

winform.onClose = function(){ stop(); }
```

取消和关闭之后，旧的任务与播放器都不会再回调，所以不需要额外的状态或计数变量。

## 接口

```aardio
import sys.mediaFoundation.streamingMp3Player;
var player,err = sys.mediaFoundation.streamingMp3Player({
    sampleRate=24000;
    channels=1;
    volume=1;
    maxPcmBytes=67108864;
    onStart=function(){ /*首次提交 PCM 到设备*/ };
    onEnd=function(){ /*余音已经播完，资源已释放*/ };
    onError=function(err){ print(err); };
});
if(!player) return print(err);
// 在之后的 MP3 数据回调中调用 player.write(data)。
// 全部输入成功结束：player.finish()。
// 来源失败：player.finish("错误信息")。
// 立即停止或窗口退出：player.close()。
player.close();
```

| 接口 | 语义 |
|---|---|
| `write(data)` | 追加 MP3 `string`/`buffer`，成功 `true`，失败 `null,err`。空块无操作，不代表 EOF。同步复制和解码，播放异步进行，调用返回后可复用输入 buffer |
| `finish()` | 输入结束，排空解码器，继续播放已提交的 PCM。幂等，不立即停止 |
| `finish(err)` | 来源失败，立即中止并触发一次 `onError`；`err` 可以是错误字符串或含 `message` 的错误表 |
| `close()` | 立即停止，释放解码器、声卡、内存及内部定时器。幂等，不触发 `onEnd` |
| `wait(timeoutMs)` | 消息泵等待播放结束；成功 `true`，失败 `null,err`。超时仅停止等待，不停止播放 |
| `state` | `buffering`、`playing`、`draining`、`ended`、`failed`、`closed` |
| `done` / `inputEnded` | 播放终结／输入结束，两者不是同一件事 |
| `receivedBytes` / `pcmBytes` | 已送入解码器的 MP3 字节数／已提交声卡的 PCM 字节数 |
| `playedSeconds` | 设备报告的播放位置，约每 20ms 更新。完成后保留最后位置，不是声学测量 |
| `format` | 输出 PCM 格式 `{codec="pcm";sampleRate;channels;bitsPerSample=16}` |
| `error` | 播放失败信息 |
| `callbackError` | 用户事件函数抛出的错误，已捕获，不向定时器外抛出 |
| `cleanupError` | 异常驱动清理错误。保留仍被设备引用的资源，可再次调用 `close()` |

`onStart`、`onEnd`、`onError` 的 `owner` 都是播放器。`onStart` 代表首次向声卡提交 PCM，不保证该时刻扬声器已经出声。暂时断供只进入 `buffering`，不会触发 `onEnd`；继续 `write` 可以续播。

## 格式与生命周期

- **输入是一条连续、固定格式的 MP3 字节流**。块可以任意切分；不接受 PCM、WAV、AAC 或把若干独立 MP3 当作播放列表拼接。
- 默认 `sampleRate=24000,channels=1`，匹配目前 `web.edgeTextToSpeech` 的 24kHz / 48kbps / 单声道 MP3。其他来源必须提供实际采样率、声道数；参数不是重采样设置，不探测格式。参数与实际格式不一致时的行为见 [mp3Decoder](https://www.aardio.com/zh-cn/docs/library/sys/mediaFoundation/mp3Decoder/)：采样率不一致时 `finish()` 报告“未解码出音频”，声道数不一致时播出的声音是错的。本机重点验收的是上述 Edge MP3 格式。
- 输出为同采样率、同声道数的 16 位 PCM。`volume` 范围为 0～1，默认 1；软件调整 PCM，不改系统音量，0 可用于静音测试。
- 使用系统默认音频设备。创建失败（例如无法创建系统解码器或打开音频设备）返回 `null,err`。
- 不做格式探测：解码器跳过无法解码的字节（有效 MP3 前面混入的无效字节不影响播放）。如果整段输入都不是 MP3（例如 HTML 错误页），`finish()` 返回“未解码出音频”并触发 `onError`。不会像 BASS 那样因探测未知数据占用大量进程地址空间。
- 一个实例对应一段音频。结束后如需播放另一段，请创建新实例。没有播放列表、自动重试、自动重连或环形缓存。
- **按本库的有限 TTS 场景保留已提交的 PCM，不持续回收已播数据**。播放正常结束或 `close` 时统一释放。`maxPcmBytes` 默认为 64 MiB，限制累计 PCM，不是 MP3 大小；超限明确失败，不覆盖旧数据、不隐式落盘。24kHz 单声道 PCM 每秒约 48000 字节，64 MiB 约 23 分钟。
- 所有方法必须在创建对象的 aardio 线程调用，不直接跨线程传递播放器。UI 用既有 `win.loopMessage()`；无可见窗口的脚本可用 `wait()` / `thread.delay()` 处理消息。不要在本播放器事件函数中调用 `wait()`。
- 应用停止时同时 `task.cancel()` 和 `player.close()`：前者停止合成，后者停止声音。窗口销毁也应执行这两项清理。可以在播放器自己的 `onEnd`、`onError` 中执行这两项清理。
- 这是独立原生播放器，不是 `WMPlayer.OCX` 的扩展。既有 `wsock.tcp.httpStreamMp3` 与各种 URL 消费者不受影响。

## 播放无声排查

`playedSeconds` 正常前进、`state` 正常结束，但音箱没有声音，最常见的原因是**本进程的音频会话被静音或音量为 0**（Windows 音量合成器中的应用音量），而不是解码或 waveOut 出错：

- waveOut 输出经过本进程的音频会话；会话静音时设备照常消费数据，位置照常前进。
- Windows 按 EXE 路径记住应用音量。`WMPlayer.OCX` 的 `settings.mute` / `settings.volume`、`MediaPlayer.MediaPlayer` 的 `Mute` 都会直接改写本会话并被记住，之后同一 EXE 的所有进程（包括 waveOut、BASS 等输出）都受影响。在同一进程里做过"静音测试"后，后续原生播放会全部无声。
- 本库的 `volume` 选项只是软件增益，不读写会话。

用 `sys.audioVolume.session()` 读取本进程的会话状态，返回 `{ volume=0..1; mute=布尔; audible=未静音且音量大于 0 }`；`sys.audioVolume.session(volume,mute)` 设置后返回同样的表，任一参数为 `null` 表示不修改：

```aardio
import sys.audioVolume.session;
var session = sys.audioVolume.session();
if(session && !session.audible){
    // 用户确认后再恢复；这会修改 Windows 记住的应用音量
    sys.audioVolume.session(1,false);
}
```

`sys.audioVolume.session` 操作的是本进程的应用音量（0～1）；`sys.audioVolume()` 创建的对象操作的是系统主音量（0～100），两者不同。也可以直接在系统托盘音量图标 » 打开音量合成器中把本程序的音量恢复为非静音。
