chore: upgrade gl3d to v1.0.0 and decode leniently
This commit is contained in:
1 parent
1a7f3cf2fc
commit
2fb58e001e
9 files changed
+101
-21
No files matched your search
@@ -5,7 +5,7 @@ go 1.27.1
|
||||
ignore ./web/node_modules
|
||||
|
||||
require (
|
||||
github.com/DiheMoe/gl3d v0.0.0-20261022172000-ae409e94c25a
|
||||
github.com/DiheMoe/gl3d v1.0.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.47.3
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.9
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.114.4
|
||||
|
||||
@@ -4,8 +4,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4 h1:m2xcjRABtG288vnlV3U/PKh/HnPURxZp50RKNUwBxM8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20261006220739-8c912ac31dd4/go.mod h1:MTUE7ZuZO89FAtAwozGg6AWD83pcdefnEq5gF82MB8s=
|
||||
github.com/DiheMoe/gl3d v0.0.0-20261022172000-ae409e94c25a h1:VIrsshIISZxzDBJ3aq5ASnfu880eYcA5vFol+YAWcM4=
|
||||
github.com/DiheMoe/gl3d v0.0.0-20261022172000-ae409e94c25a/go.mod h1:lRy4c4S/LQ5hjKoHpDSZF3NQTRkDlUUt5aIL1RzjEN4=
|
||||
github.com/DiheMoe/gl3d v1.0.0 h1:otOylwJk1efoTI8YLwcO/CE3+UWoSsN6ocftatrNNCQ=
|
||||
github.com/DiheMoe/gl3d v1.0.0/go.mod h1:lRy4c4S/LQ5hjKoHpDSZF3NQTRkDlUUt5aIL1RzjEN4=
|
||||
github.com/Microsoft/go-winio v0.6.3 h1:M/52tsoyTfwaxajLnXZm7PqkbMGRH8iSWP+8fcg9MQ4=
|
||||
github.com/Microsoft/go-winio v0.6.3/go.mod h1:ma0QpxizD4fyyzJ1C7MIblymTD2Nxz5Mho/3CepwIZg=
|
||||
github.com/aws/aws-sdk-go-v2 v1.47.3 h1:HRoT/MkNi3bjO5xF4dCzLzvVmkvRGHj3/cLINDnJejk=
|
||||
|
||||
@@ -180,10 +180,10 @@ func TestLinkRunnerFallsBackWhenAudioCannotBeAnalyzed(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLinkRunnerLogsAudioThatCannotBeDecoded(t *testing.T) {
|
||||
decodeErr := truncatedMP3Error(t)
|
||||
decodeErr := noAudioError(t)
|
||||
runWith := func(m track.Meta) (*bytes.Buffer, error) {
|
||||
logs := &bytes.Buffer{}
|
||||
d := &downloader{meta: m, audio: truncatedMP3}
|
||||
d := &downloader{meta: m, audio: noAudioMP3}
|
||||
err := newLinkRunner(d, porttest.NewMediaStore(), newTracks(), slog.New(slog.NewJSONHandler(logs, nil))).Run(t.Context(), link)
|
||||
return logs, err
|
||||
}
|
||||
|
||||
@@ -52,6 +52,9 @@ func analyze(ctx context.Context, logger *slog.Logger, t track.Track, audio io.R
|
||||
prepared := track.Prepared{Duration: fallback, Gain: 1, Lyric: lyric}
|
||||
if err == nil {
|
||||
prepared.Duration, prepared.Gain = result.Duration, result.Coefficient()
|
||||
if result.Damage != nil {
|
||||
logger.WarnContext(ctx, "音频有损坏,增益和时长按解出的部分计算", "source", t.Source, "track_id", t.ID, "report", result.Damage)
|
||||
}
|
||||
} else {
|
||||
logger.WarnContext(ctx, "音频无法解码,增益取 1、时长取元数据", "source", t.Source, "track_id", t.ID, "error", err)
|
||||
}
|
||||
|
||||
@@ -37,11 +37,13 @@ var silentFrame = append([]byte{0xFF, 0xFB, 0x90, 0x00}, make([]byte, 413)...)
|
||||
|
||||
var truncatedMP3 = append(bytes.Repeat(silentFrame, 3), silentFrame[:200]...)
|
||||
|
||||
func truncatedMP3Error(t *testing.T) error {
|
||||
var noAudioMP3 = []byte("ID3\x04\x00\x00\x00\x00\x00\x00\x00\x00")
|
||||
|
||||
func noAudioError(t *testing.T) error {
|
||||
t.Helper()
|
||||
_, err := loudness.AnalyzeReader(bytes.NewReader(truncatedMP3))
|
||||
_, err := loudness.AnalyzeReader(bytes.NewReader(noAudioMP3))
|
||||
if err == nil {
|
||||
t.Fatal("被截断的 MP3 应该无法解码")
|
||||
t.Fatal("没有音频帧的 MP3 应该无法解码")
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -83,6 +85,35 @@ func TestRunSavesWordAndTranslatedLyrics(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunStaysQuietForIntactAudio(t *testing.T) {
|
||||
source := porttest.NewSource(track.NetEase)
|
||||
source.Add("186016", meta, "", porttest.SilentWAV(time.Second))
|
||||
if _, logs, err := runLogged(t, source, sunny); err != nil || logs.Len() != 0 {
|
||||
t.Errorf("错误 = %v,日志 = %q,期望完好的音频不写日志", err, logs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunMeasuresDamagedAudioAndWarns(t *testing.T) {
|
||||
source := porttest.NewSource(track.NetEase)
|
||||
source.Add("186016", meta, "", truncatedMP3)
|
||||
tracks, logs, err := runLogged(t, source, sunny)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 3 帧、每帧 1152 个采样帧,44.1 kHz。
|
||||
if got, _ := tracks.row(sunny); got.Result.Duration >= time.Second || got.Result.Duration <= 0 {
|
||||
t.Errorf("时长 = %v,期望取解出的部分,而不是元数据的 %v", got.Result.Duration, meta.Duration)
|
||||
}
|
||||
var entry map[string]any
|
||||
if err := json.Unmarshal(logs.Bytes(), &entry); err != nil {
|
||||
t.Fatalf("日志 = %q:%v", logs, err)
|
||||
}
|
||||
report, _ := entry["report"].(map[string]any)
|
||||
if entry["level"] != "WARN" || entry["track_id"] != sunny.ID || report["truncated"] != true {
|
||||
t.Errorf("日志 = %v,期望一条带异常报告的 WARN", entry)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunFallsBackWhenAudioCannotBeAnalyzed(t *testing.T) {
|
||||
source := porttest.NewSource(track.NetEase)
|
||||
source.Add("186016", meta, "", []byte("不是音频"))
|
||||
@@ -103,7 +134,7 @@ func TestRunFallsBackWhenAudioCannotBeAnalyzed(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunLogsAudioThatCannotBeDecoded(t *testing.T) {
|
||||
decodeErr := truncatedMP3Error(t)
|
||||
decodeErr := noAudioError(t)
|
||||
sourceWith := func(m track.Meta, audio []byte) port.Source {
|
||||
source := porttest.NewSource(track.NetEase)
|
||||
source.Add("186016", m, "", audio)
|
||||
@@ -111,14 +142,14 @@ func TestRunLogsAudioThatCannotBeDecoded(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("回退", func(t *testing.T) {
|
||||
_, logs, err := runLogged(t, sourceWith(meta, truncatedMP3), sunny)
|
||||
_, logs, err := runLogged(t, sourceWith(meta, noAudioMP3), sunny)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
checkDecodeWarning(t, logs, sunny, decodeErr)
|
||||
})
|
||||
t.Run("元数据中也没有时长", func(t *testing.T) {
|
||||
_, _, err := runLogged(t, sourceWith(track.Meta{Title: "晴天"}, truncatedMP3), sunny)
|
||||
_, _, err := runLogged(t, sourceWith(track.Meta{Title: "晴天"}, noAudioMP3), sunny)
|
||||
var rejected *port.RejectedError
|
||||
if !errors.As(err, &rejected) || err.Error() != "这首歌的音频无法播放" {
|
||||
t.Errorf("错误 = %v,期望不能准备,原因带着解码的错误 %q", err, decodeErr)
|
||||
|
||||
@@ -5,6 +5,7 @@ package loudness
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
@@ -21,6 +22,7 @@ type Result struct {
|
||||
Integrated float64 // 综合响度(LUFS),全静音时为 -Inf
|
||||
Peak float64 // 采样峰值(dBFS),全静音时为 -Inf
|
||||
Duration time.Duration
|
||||
Damage slog.LogValuer // 解码时跳过或容错的异常,音频完好时为 nil;此时其余字段只反映解出的部分
|
||||
}
|
||||
|
||||
// target 的单位是 LUFS,返回的增益单位是 dB;静音时返回 0。
|
||||
@@ -64,7 +66,12 @@ func AnalyzeReader(r io.Reader) (*Result, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return an.Result()
|
||||
result, err := an.Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Damage = wav.Damage(dec)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// samples 是按声道交错的 PCM 采样,满刻度为 1,可以超出 [-1, 1]。
|
||||
|
||||
@@ -42,6 +42,9 @@ func TestAnalyzeReaderMP3MatchesReference(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Damage != nil {
|
||||
t.Errorf("完好的文件 Damage = %v,期望 nil", got.Damage.LogValue())
|
||||
}
|
||||
if math.Abs(got.Integrated-tt.integrated) > 0.05 {
|
||||
t.Errorf("Integrated = %.3f LUFS,期望 %.3f", got.Integrated, tt.integrated)
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ func mp3Frame(header ...byte) []byte {
|
||||
|
||||
var (
|
||||
silentFrame = mp3Frame(0xFF, 0xFB, 0x90, 0x00)
|
||||
layer2Frame = mp3Frame(0xFF, 0xFD, 0x90, 0x00)
|
||||
layer2Frame = mp3Frame(0xFF, 0xFD, 0x80, 0x00)
|
||||
)
|
||||
|
||||
// 准备任务在 serve 进程中解码整首歌,稳态下每次 Read 都不能分配内存。
|
||||
@@ -259,8 +259,8 @@ func TestDecodeRejectsUndecodableMP3(t *testing.T) {
|
||||
is func(error) bool
|
||||
}{
|
||||
{"Layer II", bytes.Repeat(layer2Frame, 3), "不支持的 MP3 格式", asError[*gl3d.UnsupportedError]},
|
||||
{"最后一帧被截断", bytes.Repeat(silentFrame, 4)[:3*len(silentFrame)+200], "MP3 数据被截断", asError[*gl3d.TruncatedError]},
|
||||
{"ID3 标签声明的长度超过上限", []byte("ID3\x04\x00\x00\x7f\x7f\x7f\x7f\x00\x00"), "MP3 超出解码的资源上限", asError[*gl3d.LimitError]},
|
||||
{"只有标签", []byte("ID3\x04\x00\x00\x00\x00\x00\x00\x00\x00"), "MP3 中没有音频帧", func(err error) bool { return errors.Is(err, gl3d.ErrNoAudio) }},
|
||||
{"首帧之前的数据超过上限", append([]byte("ID3\x04\x00\x00\x00\x00\x00\x00"), make([]byte, 70<<10)...), "MP3 超出解码的资源上限", asError[*gl3d.LimitError]},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -271,6 +271,34 @@ func TestDecodeRejectsUndecodableMP3(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDamageReportsSkippedMP3Data(t *testing.T) {
|
||||
decode := func(t *testing.T, file []byte) Decoder {
|
||||
t.Helper()
|
||||
dec, err := NewDecoder(bytes.NewReader(file))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := drainDecoder(dec); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dec
|
||||
}
|
||||
if got := Damage(decode(t, bytes.Repeat(silentFrame, 4))); got != nil {
|
||||
t.Errorf("完好的 MP3:Damage = %v,期望 nil", got.LogValue())
|
||||
}
|
||||
truncated := Damage(decode(t, bytes.Repeat(silentFrame, 4)[:3*len(silentFrame)+200]))
|
||||
if truncated == nil || !strings.Contains(truncated.LogValue().String(), "truncated") {
|
||||
t.Errorf("最后一帧被截断:Damage = %v,期望记下截断", truncated)
|
||||
}
|
||||
flac, err := os.ReadFile("testdata/sine.flac")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := Damage(decode(t, flac)); got != nil {
|
||||
t.Errorf("FLAC:Damage = %v,期望 nil", got.LogValue())
|
||||
}
|
||||
}
|
||||
|
||||
type failingReader struct{ err error }
|
||||
|
||||
func (r failingReader) Read([]byte) (int, error) { return 0, r.err }
|
||||
|
||||
+14
-6
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
|
||||
"github.com/DiheMoe/gl3d"
|
||||
)
|
||||
@@ -15,6 +16,7 @@ type mp3Decoder struct{ *gl3d.Decoder }
|
||||
// ctx 传 Background:取消靠 r 在 ctx 结束后读取失败(下载流,以及 prepare 中包了 ctx 的文件),gl3d 会原样返回这个错误;
|
||||
// WAV 和 FLAC 也是这样取消的,所以 Decoder 不接收 ctx。gl3d 自己的 ctx 只在每帧之前和每次读取之前检查,
|
||||
// 即使传入真实的 ctx,也至多少解码已缓冲的 64 KiB。
|
||||
// 用默认的宽松模式:解码只用来计算增益和时长,播放的是原文件,局部损坏时按解出的部分计算比整首放弃更接近实际。
|
||||
func newMP3Decoder(r io.Reader) (Decoder, error) {
|
||||
dec, err := gl3d.NewDecoder(context.Background(), r)
|
||||
if err != nil {
|
||||
@@ -28,6 +30,18 @@ func (d mp3Decoder) Read(p []float64) (int, error) {
|
||||
return n * d.Channels(), mp3Error(err)
|
||||
}
|
||||
|
||||
// Damage 返回解码时跳过或容错的异常,用于写入日志;音频完好时返回 nil。解码结束后才是最终结果。
|
||||
func Damage(d Decoder) slog.LogValuer {
|
||||
mp3, ok := d.(mp3Decoder)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if report := mp3.Report(); !report.Clean() {
|
||||
return report
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// io.EOF 原样返回,调用方靠它判断读完。
|
||||
// 每次 Read 都经过这里:errors.As 的目标变量要在提前返回之后声明,否则它们会逃逸到堆上,使每次 Read 多分配四个对象。
|
||||
func mp3Error(err error) error {
|
||||
@@ -36,8 +50,6 @@ func mp3Error(err error) error {
|
||||
}
|
||||
var (
|
||||
unsupported *gl3d.UnsupportedError
|
||||
truncated *gl3d.TruncatedError
|
||||
corrupt *gl3d.CorruptError
|
||||
limit *gl3d.LimitError
|
||||
)
|
||||
switch {
|
||||
@@ -45,10 +57,6 @@ func mp3Error(err error) error {
|
||||
return fmt.Errorf("wav: MP3 中没有音频帧: %w", err)
|
||||
case errors.As(err, &unsupported):
|
||||
return fmt.Errorf("wav: 不支持的 MP3 格式: %w", err)
|
||||
case errors.As(err, &truncated):
|
||||
return fmt.Errorf("wav: MP3 数据被截断: %w", err)
|
||||
case errors.As(err, &corrupt):
|
||||
return fmt.Errorf("wav: MP3 数据损坏: %w", err)
|
||||
case errors.As(err, &limit):
|
||||
return fmt.Errorf("wav: MP3 超出解码的资源上限: %w", err)
|
||||
default:
|
||||
|
||||
Reference in new issue
Block a user