package voice import ( "fmt" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/transcode" ) // 本文件承载「**能不能播**」与「该转成什么」这两条**格式级**判据(design/24 §10.6、 // design/25 §3.1/§3.2)。 // // 🔴 判据纪律:可播与否必须是**格式级结论**(格式名 → 已知支持表),**绝不**靠「扩展名 // 看起来像音频」,也绝不靠「试着播一下」。设计里的同一条话(design/24 §10.14-3)说的是 // 同一件事:宣称能播就要有依据,播不了就照实提示。 // playableFormats 是「浏览器与常见原生播放器**能直接播**」的格式集合,取值口径与 // mediafetch.AudioFormatOf 归一出的格式名同一份(小写、无点)。 // // 依据 = design/25 §3.2 的 MDN《Audio codecs》支持表(规范/文档级结论): // wav(PCM 基线)、mp3、ogg/oga/opus、m4a/aac(MP4 家族)、flac、webm 均可播。 // // `amr` / `amr-wb` **刻意不在**集合内:Chrome 的 AMR 支持为 **No**(只有 ChromeOS 支持 // AMR-NB/WB)⇒ QQ OneBot v11 的语音在 Web 上点不开,必须转码(design/24 §10.14-2 的 // 核实结论,唯一必需的那条路 = `amr → wav`)。 // // ⚠️ **诚实边界(别当已实测)**:上表是规范/文档级结论,**不是**本项目在目标客户端上 // 的逐浏览器实测(design/25 §3.2 已登记同一条「未确证」)。实测不符则以实测为准, // 照实提示、不假装。对本函数而言,判错的后果是**有界的**:判「可播」⇒ 原样落副本, // 客户端播不了会照实提示;判「不可播」⇒ 走转码,没有转码路就直接不落副本。 var playableFormats = map[string]bool{ "wav": true, "mp3": true, "ogg": true, "oga": true, "opus": true, "m4a": true, "aac": true, "flac": true, "webm": true, } // transcodeTargets 是「不可播格式 → 该转成的目标格式」的唯一登记点(design/25 §3.1: // 本期只发布 `amr → wav` 一条路;新增一条路 = 这里加一行 + transcode 包加一个注册项)。 // // amr-wb 也登记目标(同族、目标同样是 wav),但**本期没有实现**:让 `transcode` 层如实 // 回答「注册表里没有这条路」(design/25 §3.3 把它后置),比在这里假称「我们不会做」 // 更能说清问题。silk 刻意**不登记**:design/24 §10.14-1 的既有裁决是微信语音不取字节, // 且纯 Go SILK 解码器生态不成熟(design/25 §3.3)。 var transcodeTargets = map[string]transcode.Format{ "amr": transcode.FormatWAV, "amr-wb": transcode.FormatWAV, } // Playable 报告某个格式名(小写、无点)的字节能否**直接被播放器消费**。格式不认识 // ⇒ false(deny-by-default:不认识的类型绝不当成可播)。 func Playable(format string) bool { return playableFormats[normalize(format)] } // TranscodeTarget 给出「该格式要落可播副本时应当转成什么」。ok=false 表示**没有登记的 // 目标**(格式可播、或这条路我们不做)——调用方据此如实提示,绝不自己发明一条路。 func TranscodeTarget(format string) (transcode.Format, bool) { target, ok := transcodeTargets[normalize(format)] return target, ok } // NeedsTranscode 报告这个格式要落副本是否**必须先转码**(= 不可播且登记了目标)。 // 它刻意不等价于 `!Playable`:不可播但没登记目标的格式(如 silk)属于「做不了」, // 走 ErrNotPlayable,而不是走一次注定失败的转码。 func NeedsTranscode(format string) bool { if Playable(format) { return false } _, ok := TranscodeTarget(format) return ok } // PlayableFormats 返回当前认定为可播的格式名(**发现**用;顺序无保证,调用方不得依赖 // 顺序)。它存在的理由是让「这份表是活的」有据可查(面板/日志能列出判据面)。 func PlayableFormats() []string { out := make([]string, 0, len(playableFormats)) for f := range playableFormats { out = append(out, f) } return out } // normalize 把格式名归一成小写、去空白、去开头的点(`.amr` → `amr`)。归一只有这一处, // 与 `transcode.FormatOf` 同口径(design/25 §6.2:三处格式名必须同源)。 func normalize(format string) string { return strings.ToLower(strings.TrimPrefix(strings.TrimSpace(format), ".")) } // errNotPlayable 给 ErrNotPlayable 附上格式,便于日志与测试按 errors.Is + 文案双判。 func errNotPlayable(format string) error { return fmt.Errorf("voice: prepare: format %q: %w", format, ErrNotPlayable) }