Conversation
|
这周末有事不在家,可能下周才有空看看。 另外我建议给我一个非 master 分支可编辑的 collaborator 权限, 这样我可以直接把改动推到这个分支 |
感觉先针对 audio 分支提 pr 就可以了,我直接合并后应该会自动同步到这个 pr 中。 |
|
所以为啥引入了个依赖没去用sokol_audio.h,是有啥限制吗 |
|
sokol_audio.h 只有设备驱动,相当于图形层只给了你一个 framebuffer 。你还需要实现 音频数据解码、混音… |
|
soloud 最有价值的部分是混音,其次是 filter 和 audio source ,这两个迟早也是需要的。 |
feat(build/luamake): support audio
|
我想再尝试一下集成 https://github.com/mackron/miniaudio
soloud 提供的那些更丰富的外层功能就没了。不过如果需要这些玩具(例如文字转语音),soloud 似乎也可以用 miniaudio 作 backend |
|
不管是哪个 backend 在 wasm 都一样需要在主线程创建,通过用户手势触发播放。音频线程初始化流程似乎比较麻烦。 |
miniaudio 有更 low level 的线程控制 api ,或许可以和 ltask 结合的更好一些。初始化部分已经有机制可以放去主线程运行, 但 miniaudio 没有从内存加载数据的 api ,而必须额外实现一个 VFS 。所以我得先导出一套 C 接口的 zip reader ,才能为 miniaudio 实现对应接口。 |
|
我已经更换成 miniaudio ,还有几项工作需要做:
下午带娃去攀岩,晚上继续搞。 |
|
luamake 修好了。emcc 还需要 @yuchanns 调整一下。 |
|
不在家所以远程指挥 copilot 修了一下。 你看着 cherry pick 一下。 还有个问题是 https://yuchanns.github.io/soluna/examples/audio/ 依旧会卡住,我没法看 f12 看不到有没有报错。不过我猜测是 io 卡住 具体改这两个就行 |
|
我正在尝试把 audio 初始化放在主线程。我发现最简单的方法还是一开始就初始化好,而不是惰性初始化。 初始化 audio 一定会触发权限请求吗? |
|
根据 mdn 文档看,创建 audio context 只需要在主线程即可。是播放需要权限 |
|
d04c5fa 目前采用了最简单的修改方法:在 render 初始化图形设备的同时,把声音设备也初始化。这个步骤在游戏主程序的最前面。 然后在 audio 服务被调用 init 时,去 render 服务获取 engine 对象指针就可以了。 所以 |
EM_JS(int, saudio_js_init, (int sample_rate, int num_channels, int buffer_size), {
Module._saudio_context = null;
Module._saudio_node = null;
if (typeof AudioContext !== 'undefined') {
Module._saudio_context = new AudioContext({
sampleRate: sample_rate,
latencyHint: 'interactive',
});
}
else {
Module._saudio_context = null;
console.log('sokol_audio.h: no WebAudio support');
}
if (Module._saudio_context) {
console.log('sokol_audio.h: sample rate ', Module._saudio_context.sampleRate);
Module._saudio_node = Module._saudio_context.createScriptProcessor(buffer_size, 0, num_channels);
Module._saudio_node.onaudioprocess = (event) => {
const num_frames = event.outputBuffer.length;
const ptr = __saudio_emsc_pull(num_frames);
if (ptr) {
const num_channels = event.outputBuffer.numberOfChannels;
for (let chn = 0; chn < num_channels; chn++) {
const chan = event.outputBuffer.getChannelData(chn);
for (let i = 0; i < num_frames; i++) {
chan[i] = HEAPF32[(ptr>>2) + ((num_channels*i)+chn)]
}
}
}
};
Module._saudio_node.connect(Module._saudio_context.destination);
// in some browsers, WebAudio needs to be activated on a user action
const resume_webaudio = () => {
if (Module._saudio_context) {
if (Module._saudio_context.state === 'suspended') {
Module._saudio_context.resume();
}
}
};
document.addEventListener('click', resume_webaudio, {once:true});
document.addEventListener('touchend', resume_webaudio, {once:true});
document.addEventListener('keydown', resume_webaudio, {once:true});
return 1;
}
else {
return 0;
}
})我看了下 pacman.c 的处理方案是在初始化全局监听任意用户手势触发 |
|
下周如果 mac 和 linux 都正常,我就合并到 master 。web 可以慢慢弄。 然后就可以完善 audio 的具体功能。 |
~~ 看起来我们可以自己注册到 window.AudioContext, 这样这个方法就会返回 ~~ 不行, 这两个方法是互斥的
|
我尝试在 api.init 里进行初始化,结果会造成主线程空转. 永远无法到下一步,猜测是一直出于 MA_BUSY 状态. |
我想,这里的初始化流程还是期待在整个环境都启动起来之后,所有的 worker 都可以工作。而这里的主线程中调用这段代码, 如果以上猜测成立,我不知道怎样的环境这个函数才能正确工作:即保证前面的 有直接用 C 代码成功初始化 mini audio 的例子吗?我们只需要模仿这样的例子梳理流程也能正确初始化。 另外,我想了一个 workaround : 既然 emscripten_create_audio_context 必须在主线程调用,那我们就让它在主线程调,比如在上面提到的 api.init() 。这就相当于你说的预先 “注册到 window.AudioContext” ,但不走常规路径。 可以在集成 miniaudio.h 的地方, 因为它并没有真的调用 pDevice->webaudio.initResult = MA_BUSY;
{
emscripten_start_wasm_audio_worklet_thread_async(pDevice->webaudio.audioContext, pStackBuffer, MA_AUDIO_WORKLETS_THREAD_STACK_SIZE, ma_audio_worklet_thread_initialized__webaudio, pInitParameters);
}
while (pDevice->webaudio.initResult == MA_BUSY) { emscripten_sleep(1); }即:
|
我能想到的可能性是:audio init 需要在主线程运行,而不是 callback 中(这一点 sokol_main() 符合要求) 但是,它同时还需要在 emscripten_set_main_loop 或 emscripten_request_animation_frame_loop 之后?即,所有的 callback 都注册完毕? 如果需要满足以上两点,就不可以直接使用 sokol_main ,因为 sokol 默认的封装是这样的: int main(int argc, char* argv[]) {
sapp_desc desc = sokol_main(argc, argv);
_sapp_emsc_run(&desc);
return 0;
}设置回调函数是通过 _sapp_emsc_run 在 sokol_main 之后发生的。 但是,sokol 还是留了后门。可以定义 SOKOL_NO_ENTRY ,然后自定义一个 main 函数,主动调用 只不过,如果以上猜想若成立的话,mini audio 的初始化流程就对使用过于苛刻了。 |
|
miniaudio 自带例子: https://github.com/mackron/miniaudio/blob/master/examples/simple_playback_sine.c 从这个代码上看的话, 所有初始化是在 我晚上到家试试. |
|
这个例子调用 ma_device_init 的时机和上面在 sokol_main 里面调用一模一样。所以问题和猜想的不一样。 |
|
我写了个简单 demo: #if defined(__EMSCRIPTEN__)
#define SOKOL_GLES3
#else
#define SOKOL_METAL
#endif
#define SOKOL_IMPL
#define SOKOL_APP_IMPL
#define SOKOL_LOG_IMPL
#include "sokol_app.h"
#include "sokol_log.h"
#define MINIAUDIO_IMPLEMENTATION
#define MA_NO_DECODING
#define MA_NO_ENCODING
#include "miniaudio.h"
#include <stdbool.h>
#include <stdio.h>
#define DEVICE_FORMAT ma_format_f32
#define DEVICE_CHANNELS 2
#define DEVICE_SAMPLE_RATE 48000
#define TONE_FREQUENCY 220.0
#define TONE_AMPLITUDE 0.2
typedef struct {
ma_waveform sine_wave;
ma_device device;
bool audio_started;
bool audio_failed;
} app_state_t;
static app_state_t g_app;
static void data_callback(ma_device *pDevice, void *pOutput, const void *pInput,
ma_uint32 frameCount) {
ma_waveform_read_pcm_frames((ma_waveform *)pDevice->pUserData, pOutput,
frameCount, NULL);
(void)pInput;
}
static bool start_audio(void) {
if (g_app.audio_started) {
return true;
}
if (g_app.audio_failed) {
return false;
}
if (ma_device_start(&g_app.device) != MA_SUCCESS) {
printf("Failed to start playback device.\n");
ma_device_uninit(&g_app.device);
ma_waveform_uninit(&g_app.sine_wave);
g_app.audio_failed = true;
return false;
}
printf("Audio started.\n");
g_app.audio_started = true;
return true;
}
static void init(void) {
g_app = (app_state_t){0};
ma_device_config device_config;
ma_waveform_config sine_wave_config;
ma_result result;
sine_wave_config = ma_waveform_config_init(
DEVICE_FORMAT, DEVICE_CHANNELS, DEVICE_SAMPLE_RATE, ma_waveform_type_sine,
TONE_AMPLITUDE, TONE_FREQUENCY);
ma_waveform_init(&sine_wave_config, &g_app.sine_wave);
device_config = ma_device_config_init(ma_device_type_playback);
device_config.playback.format = DEVICE_FORMAT;
device_config.playback.channels = DEVICE_CHANNELS;
device_config.sampleRate = DEVICE_SAMPLE_RATE;
device_config.dataCallback = data_callback;
device_config.pUserData = &g_app.sine_wave;
result = ma_device_init(NULL, &device_config, &g_app.device);
if (result != MA_SUCCESS) {
printf("Failed to open playback device.\n");
ma_waveform_uninit(&g_app.sine_wave);
g_app.audio_failed = true;
sapp_quit();
}
printf("Device Name: %s\n", g_app.device.playback.name);
printf("App initialized. Click / tap / press any key to start audio.\n");
}
static void frame(void) {}
static void cleanup(void) {
ma_device_uninit(&g_app.device);
ma_waveform_uninit(&g_app.sine_wave);
}
static void event(const sapp_event *ev) {
switch (ev->type) {
case SAPP_EVENTTYPE_MOUSE_DOWN:
case SAPP_EVENTTYPE_TOUCHES_BEGAN:
case SAPP_EVENTTYPE_KEY_DOWN:
if (!g_app.audio_started && !g_app.audio_failed) {
start_audio();
}
break;
default:
break;
}
}
sapp_desc sokol_main(int argc, char *argv[]) {
(void)argc;
(void)argv;
return (sapp_desc){
.init_cb = init,
.frame_cb = frame,
.cleanup_cb = cleanup,
.event_cb = event,
.width = 640,
.height = 480,
.window_title = "sokol + miniaudio + emscripten",
.logger.func = slog_func,
};
}然后使用 然后移植到 soluna : yuchanns@50cf896
~~怎么是先 frame 然后才 app_init ~~ 和这个没关系, 我对上面那个正常例子进行 assert 发现是一样的调用栈: |
|
我发现上面这个例子其实也有问题, 只是我忘了添加 换句话说:
所以应该就是和 sokol 的运行时有关系, |
|
我把 #88 patch 我的分支好像有效果。我再调试下试试……
apply 这个 patch 后可以运行了。但是……依旧没声音,明天再看看 patch 之后的这段代码是有效果的,会发出声音: yuchanns@50cf896 但是 soluna 本身的播放还是没有声音。我怀疑要么是手势问题要么是 device 没有在手势里 ma_device_start |
你写的 sokol 的例子和 miniaudio 官方的例子是不一样的。 sokol 的例子把初始化放在了 callback 里,肯定会触发问题。 如果要做到等价,应该写在 sokol_main 里,而不是写在被注册到 init callback 的 init 函数里。这样应该是不需要那个 patch 的。 sokol init callback 实质上就是单次触发的 frame callback 。 我的问题是这个:
api.init 是由 https://github.com/cloudwu/soluna/blob/master/src/entry.c#L904 触发的,即在 sokol_main 里。看起来没有 |
你说的对, 我改成在 sokol_main 里初始化, 是可以正常工作的 |
这个问题可以跳过了,我今天 revert 这段代码,又可以运行了,不知道为什么,但是确实可以了。所以可以按照你说的, 在 init_setting 里初始化, 然后将参数传递给 start 整个链路应该没问题了。顺利的话我下午提交 pr |
我想好了方案现在改。我觉得 web 版没声音可能是因为我前面偷懒没有把读 zip 的 vfs 初始化(默认实现没有传入 zipfile list)。 不过改在 init_setting 里面初始化,又不太好先初始化这个。所以我还需要改成两步初始化:先把声音设备创建好,后面再初始化 vfs 。 |
|
应该是你说的这个问题了。我注意到 ma_engine_play_sound 的返回值没有被处理, 我添加了错误处理可以看到播放是失败了: |
|
a983873 把 audio device 的初始化移到了最前面。 增加了 audio.init_vfs 用于注入 vfs 接口支持 zipfiles 。如果不调用 init_vfs 就使用 local files 版的 vfs 。查看过 miniaudio 的实现,它只保存了 VFS 的指针,所以这里的动态替换 vfs 是可行的。 audio service 可以多次获取 sound sample 列表,这可以允许多个不同服务调用各自的 soluna.play_sound 都能工作。 |
如果考虑最大兼容性,注入用户手势监听授权后,保持现在 master 里的 Script Node Processor 主线程播放的方式应该也可以正常工作,并且可以支持 Safari, 还有 extlua 特性。 当然也许也可以试试 apply 前面 copilot 给的 patch, 然后在 init cb 里进行初始化, 这样也许不需要开启 |
我晚上开个分支试试前面提到的另一个 workaround 。这里的初始化有两个约束:
这两个约束相互制约才造成了困境。我试试把两者拆分开来:
我猜想这样就不再需要开启 |
|
@yuchanns 最新的 audioinit 分支,我尝试在 emcc 版中两步初始化 audio device 。方法是:
这样做是建立在猜想:只有 emscripten_create_audio_context 需要在主线程运行,而其余步骤在其它线程运行更好。因为独立线程不受主线程 callback 函数的限制。 把 miniaudio 的初始化流程分拆为两步的 trick 是把 emscripten_create_audio_context 通过宏转换为 emscripten_create_audio_context_ ,让 miniaudio 的代码调用获取之前创建的 audio context 。如果这个方法测试通过,后续可能还需要进一步创建支持 context 的用户配置项(通过 settings )。暂时传入的空指针使用默认项。 |
|
我等下部署试试 |
|
在修复了一点编译错误后我进行部署,结果是不行。不开 -fwasm_exceptions 直接报 js frame 的错, 开了变成在 audio service 里空转等待 可以到 yuchanns 体验 |
看来这个方案不行。只解决了第一个问题:emscripten_create_audio_context 需要在主线程运行;没有解决第二个问题:剩下的部分正确调度。 但我比较奇怪,如果和之前一样的话,那么问题 ”js frame 错误" 卡在 emscripten_sleep 上。但当我把调用放在 audio service 的话,看起来是在一个独立线程里面。那么 emscripten_sleep 对独立线程也又限制吗?如果是,只能说明 emscripten_sleep 和 pthread 不兼容,否则该限制有点大。 回到 master 分支之前开 -fwasm_exceptions 正确的版本的话(即整个 miniaudio 初始化都在 ltask 启动之前,尚未创建任何 pthread 线程),还有一点没弄明白:看起来即使在 main() 里调用 emscripten_sleep 也有限制?这有点奇怪,因为调用 emscripten_sleep 的时间点,只用 emscripten_start_wasm_audio_worklet_thread_async 启动了一个 worklet,并不设计任何 pthread 线程。这时不给调用 emscripten_sleep 的?也就是说,即使是 miniaudio 官方的 demo 其实也是不开 -fwasm_exceptions 就跑不起来?还是说,miniaudio 这里的实现本身不规范? |
|
根据文档说明 em sleep 其实是给 wasm worker 用的。另外本身在 main 中是可以使用 em sleep 的。其实这里的问题不是不能 sleep ,应该是等不到 worklet 启动,因此一直在空转等 ready。 |
所以,看起来解决方法很简单:用 #define 把 emscripten_sleep 换成 pthread 的 sleep 就可以了 :) 这样就可以在 pthread 线程里切出去等待。 如果可行,那么最早的版本,利用 ltask 的 mainthread api 就也是可行的了。 |
|
这想法有点意思,我明天试试 :) |


初步集成 soloud 用于声音播放。
soloud 功能非常齐全,但我不想一次导入所有的功能,考虑:
待非 windows 版本测试通过后,计划马上跟进的功能有:
可以考虑以后集成的特性:
目前亟待完成的工作,麻烦 @yuchanns 看看:
注:虽然 soloud 默认支持 mp3, flac ,但我觉得暂时意义不大。非压缩的 wav 和有损压缩的 ogg 已经基本能满足一般需求。所以我专门写了 https://github.com/cloudwu/soluna/blob/audio/src/soloudwavonly.h 用于剥离 mp3 和 flac 。这也大约可以让引擎编译后少 200Kb 左右的体积。
另外,soloud 的 C api 从官方版本做了裁减,可能日后根据需要再增加。(官方版本导出了所有 C++ API)
为了方便写 makefile ,我采用了和 yoga 集成时相同的方案:使用单一源文件 https://github.com/cloudwu/soluna/blob/audio/src/soloudone.cpp 编译。如果在做 luamake 脚本时有问题,可以继续讨论如何处理构建文件。
目前在 audio 分支上,有 test/audio.lua 可供测试。它启动后,会播放一个音频文件。该音频文件从 https://www.wavsource.com/ 下载。看起来没有版权问题。