Skip to content

音频服务 ​

AudioService 私有持有音频源、用于分析的 SoundData、谱面时间、播放状态、速度、音量与当前解码任务。它不读取 music、music_data、music_play、time 或 beat 全局。启动入口注入 LÖVE 后端与谱面接口,测试可以提供内存实现。

时间与播放 ​

所有时间接口都用秒。谱面时间为 getCurrentTime();音频位置为谱面时间减去谱面的 offset/1000。getDuration() 是音频长度加 offset 后的谱面终点;getRawDuration() 是音频原长度。编辑器定位限制在谱面起点和终点之间;菜单预览从原音频第 0 秒试听,因此负 offset 可对应负的谱面时间。正 offset 时,播放时钟先推进,达到音频起点后音频才开始发声。

seek(seconds, {pause=true}) 暂停并定位,setCurrentBeat(value, options) 按当前 BPM 表定位。当前节拍、总节拍始终从时钟和谱面计算;修改 BPM/offset、撤销重做和切换谱面后不会保留另一份旧的节拍状态。缓存仅用于复用同一位置的换算,接收谱面变更通知后失效。

pause() 立即暂停原生音频源;resume() 恢复;stop() 停止并将谱面时间归零。播放到结尾自动暂停并停在结尾,再次恢复时从起点开始。setRate(rate) 同时影响音频音高/速度和谱面时间推进,setVolume(value) 使用 0 到 1 的音量,读取当前值用 getRate()/getVolume()。main.lua 统一调用 update(dt),其他模块不要重复推进时钟。暂停期间不反复定位音频;播放偏差达到 50ms 时才补正。

加载与资源归属 ​

load(path, {preview=true}) 创建流式音频源并替换当前歌曲,菜单预览循环播放。默认请求后台解码;每个任务拥有独立通道,启动入口提供文件快照供解码使用,不依赖菜单临时挂载的目录;换歌或卸载后不会接收旧任务的结果。load 返回 true 或 false, error,音频源构造失败时保留原资源。分析数据解码失败可通过 getLoadError() 或 audio:error 取得错误,不能把尚未解码完成误判为没有音频源。

进入编辑器调用 prepareEditor():已有 SoundData 时直接复用,尚未完成时同步解码并作废旧任务引用,随后关闭预览循环、暂停并定位到谱面起点。菜单 FFT、波形、声纹图和 BPM 测量共享服务提供的数据,不分别加载同一歌曲。

unload() 停止并释放当前 Source,清空数据和任务引用。SoundData 的已有分析使用方可以继续持有自己的引用,服务不会在工作线程使用期间强行释放它。均衡器通过 setEffect(name, parameters) 设置效果,传 false 关闭;已开启的效果会应用到下一首歌曲。

setSource(source) 用于接管外部构造的源,例如内存音频;它从源读取时长并重置播放状态。setSoundData(data) 提供相应的分析数据。getSource() 保留给兼容或底层扩展,返回的原生对象不应由调用方直接执行播放/暂停/定位,避免绕过服务时钟。常规插件只需服务命令和只读的 getSoundData()。

插件迁移与通知 ​

旧的音频全局及 beat.nowbeat/allbeat 已移除。beat 仍提供节拍格式、分度和 BPM 换算工具,播放位置则通过 ctx.audio 访问。setCurrentTime 委托定位,setPlaying 委托原生播放/暂停;不再保留独立的 setDuration/setAllBeat,时长由资源和 offset 推导,总节拍由 BPM 表推导。

lua
local unsubscribe
return {
    name = 'audio example',
    init = function(ctx)
        unsubscribe = ctx.eventBus:on('audio:decoded', function(event)
            local data = ctx.audio:getSoundData() -- 数据可能为空,先判断再分析。
            if data then
                -- 根据 event.revision 更新本插件的分析缓存。
            end
        end)
    end,
    destroy = function()
        if unsubscribe then unsubscribe(); unsubscribe = nil end
    end,
}
通知参数内容
audio:loaded{path, revision},资源替换或卸载后广播;卸载时 path 为空
audio:decoded{path, revision},分析数据更新后广播
audio:playing_changed{playing},服务播放状态变化后广播
audio:error{path, error},加载、解码或效果设置失败后广播

服务通过 init({chart, backend, events, report}) 装配;chart 提供 getOffset/getBpmCount/toBeat/toTime,backend 提供 newSource/newSoundData/newThread/newChannel/setEffect,可选的 readAudio(path) 返回供解码使用的文件快照。AudioService.new(options) 创建独立实例,各实例不共享音频状态。纯内存测试可不传事件总栈,此时节拍每次现算。

验证 ​

tests/audio_service.lua 验证独立状态、即时暂停、定位、offset、变速、资源替换、失败处理、旧解码结果隔离和效果。love tests/audio_service_love 使用真实 Source 与线程验证流式加载、解码复用和播放生命周期,仅在测试目录临时写入合成 WAV,结束后删除。

love tests/editor_input_love 验证服务迁移后的进度条、滚轮、菜单及波形/声纹图绘制;love tests/bpm_love 验证测量插件读取服务数据及结果应用/撤销;love tests/spectrogram_love 验证渲染、缩放和音频替换。