From 4a90d3f38a623f40fa6cc4f6fce661a0b3567d69 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 12 Apr 2026 04:53:37 +0700 Subject: [PATCH] fix: improve ALAC M4A quality parsing --- go_backend/metadata.go | 74 +++++++++++++++++++++++-- go_backend/metadata_m4a_quality_test.go | 49 ++++++++++++++++ 2 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 go_backend/metadata_m4a_quality_test.go diff --git a/go_backend/metadata.go b/go_backend/metadata.go index fb79d330..67453f76 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -1423,16 +1423,82 @@ func GetM4AQuality(filePath string) (AudioQuality, error) { // [28:32] samplerate (16.16 fixed-point) sampleRate := int(buf[28])<<8 | int(buf[29]) bitDepth := int(buf[22])<<8 | int(buf[23]) - if bitDepth <= 0 { - bitDepth = 16 - if atomType == "alac" { - bitDepth = 24 + + if atomType == "alac" { + if alacBitDepth, alacSampleRate, ok := readALACSpecificConfig(f, sampleOffset, fileSize); ok { + if alacBitDepth > 0 { + bitDepth = alacBitDepth + } + if alacSampleRate > 0 { + sampleRate = alacSampleRate + } } } + if bitDepth <= 0 { + bitDepth = 16 + } + return AudioQuality{BitDepth: bitDepth, SampleRate: sampleRate}, nil } +func readALACSpecificConfig(f *os.File, sampleOffset, fileSize int64) (int, int, bool) { + if sampleOffset < 4 { + return 0, 0, false + } + + sampleEntryHeader, err := readAtomHeaderAt(f, sampleOffset-4, fileSize) + if err != nil { + return 0, 0, false + } + + childStart := sampleOffset + 32 + childEnd := sampleEntryHeader.offset + sampleEntryHeader.size + if childStart >= childEnd { + return 0, 0, false + } + + configHeader, found, err := findAtomInRange(f, childStart, childEnd-childStart, "alac", fileSize) + if err != nil || !found { + return 0, 0, false + } + + payloadSize := configHeader.size - configHeader.headerSize + if payloadSize <= 0 { + return 0, 0, false + } + + payload := make([]byte, payloadSize) + if _, err := f.ReadAt(payload, configHeader.offset+configHeader.headerSize); err != nil { + return 0, 0, false + } + + return parseALACSpecificConfig(payload) +} + +func parseALACSpecificConfig(payload []byte) (int, int, bool) { + if len(payload) < 24 { + return 0, 0, false + } + + bitDepth := int(payload[5]) + sampleRate := int(binary.BigEndian.Uint32(payload[20:24])) + if bitDepth > 0 && sampleRate > 0 { + return bitDepth, sampleRate, true + } + + // Some encoders prepend 4 bytes before the ALACSpecificConfig payload. + if len(payload) >= 28 { + bitDepth = int(payload[9]) + sampleRate = int(binary.BigEndian.Uint32(payload[24:28])) + if bitDepth > 0 && sampleRate > 0 { + return bitDepth, sampleRate, true + } + } + + return 0, 0, false +} + type atomHeader struct { offset int64 size int64 diff --git a/go_backend/metadata_m4a_quality_test.go b/go_backend/metadata_m4a_quality_test.go new file mode 100644 index 00000000..df3b8cb0 --- /dev/null +++ b/go_backend/metadata_m4a_quality_test.go @@ -0,0 +1,49 @@ +package gobackend + +import "testing" + +func TestParseALACSpecificConfigStandardPayload(t *testing.T) { + payload := make([]byte, 24) + payload[5] = 24 + payload[20] = 0x00 + payload[21] = 0x00 + payload[22] = 0xac + payload[23] = 0x44 + + bitDepth, sampleRate, ok := parseALACSpecificConfig(payload) + if !ok { + t.Fatal("expected standard ALAC payload to parse") + } + if bitDepth != 24 { + t.Fatalf("bitDepth = %d, want 24", bitDepth) + } + if sampleRate != 44100 { + t.Fatalf("sampleRate = %d, want 44100", sampleRate) + } +} + +func TestParseALACSpecificConfigPayloadWithLeadingFourBytes(t *testing.T) { + payload := make([]byte, 28) + payload[9] = 16 + payload[24] = 0x00 + payload[25] = 0x00 + payload[26] = 0xbb + payload[27] = 0x80 + + bitDepth, sampleRate, ok := parseALACSpecificConfig(payload) + if !ok { + t.Fatal("expected offset ALAC payload to parse") + } + if bitDepth != 16 { + t.Fatalf("bitDepth = %d, want 16", bitDepth) + } + if sampleRate != 48000 { + t.Fatalf("sampleRate = %d, want 48000", sampleRate) + } +} + +func TestParseALACSpecificConfigRejectsShortPayload(t *testing.T) { + if _, _, ok := parseALACSpecificConfig(make([]byte, 12)); ok { + t.Fatal("expected short ALAC payload to be rejected") + } +}