2020-06-08 03:29:51 +03:00
|
|
|
package mp3
|
|
|
|
|
|
|
|
// TODO: vbri
|
|
|
|
|
|
|
|
import (
|
2021-11-19 13:55:45 +03:00
|
|
|
"fmt"
|
|
|
|
|
2021-08-17 13:06:32 +03:00
|
|
|
"github.com/wader/fq/format"
|
|
|
|
"github.com/wader/fq/pkg/decode"
|
2022-07-16 19:39:57 +03:00
|
|
|
"github.com/wader/fq/pkg/interp"
|
2020-06-08 03:29:51 +03:00
|
|
|
)
|
|
|
|
|
2021-11-17 18:46:10 +03:00
|
|
|
var headerFormat decode.Group
|
|
|
|
var footerFormat decode.Group
|
|
|
|
var mp3Frame decode.Group
|
2020-06-08 03:29:51 +03:00
|
|
|
|
|
|
|
func init() {
|
2022-07-16 19:39:57 +03:00
|
|
|
interp.RegisterFormat(decode.Format{
|
2020-06-08 03:29:51 +03:00
|
|
|
Name: format.MP3,
|
2022-06-01 17:55:55 +03:00
|
|
|
ProbeOrder: format.ProbeOrderBinFuzzy, // after most others (silent samples and jpeg header can look like mp3 sync)
|
2020-06-08 03:29:51 +03:00
|
|
|
Description: "MP3 file",
|
|
|
|
Groups: []string{format.PROBE},
|
|
|
|
DecodeFn: mp3Decode,
|
2021-12-09 19:15:21 +03:00
|
|
|
DecodeInArg: format.Mp3In{
|
|
|
|
MaxUniqueHeaderConfigs: 5,
|
2023-01-25 17:27:31 +03:00
|
|
|
MaxUnknown: 50,
|
2021-12-09 19:15:21 +03:00
|
|
|
MaxSyncSeek: 4 * 1024 * 8,
|
|
|
|
},
|
2020-06-08 03:29:51 +03:00
|
|
|
Dependencies: []decode.Dependency{
|
2021-11-17 18:46:10 +03:00
|
|
|
{Names: []string{format.ID3V2}, Group: &headerFormat},
|
2021-11-19 18:44:06 +03:00
|
|
|
{
|
|
|
|
Names: []string{
|
|
|
|
format.ID3V1,
|
|
|
|
format.ID3V11,
|
|
|
|
format.APEV2,
|
|
|
|
},
|
|
|
|
Group: &footerFormat,
|
|
|
|
},
|
2021-11-17 18:46:10 +03:00
|
|
|
{Names: []string{format.MP3_FRAME}, Group: &mp3Frame},
|
2020-06-08 03:29:51 +03:00
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-05-20 16:10:41 +03:00
|
|
|
func mp3Decode(d *decode.D, in any) any {
|
2021-12-09 19:15:21 +03:00
|
|
|
mi, _ := in.(format.Mp3In)
|
|
|
|
|
2021-11-19 13:55:45 +03:00
|
|
|
// things in a mp3 stream usually have few unique combinations of.
|
|
|
|
// does not include bitrate on purpose
|
|
|
|
type headerConfig struct {
|
|
|
|
MPEGVersion int
|
|
|
|
ProtectionAbsent bool
|
|
|
|
SampleRate int
|
|
|
|
ChannelsIndex int
|
|
|
|
ChannelModeIndex int
|
|
|
|
}
|
|
|
|
uniqueHeaderConfigs := map[headerConfig]struct{}{}
|
2023-01-25 17:27:31 +03:00
|
|
|
knownSize := int64(0)
|
2021-11-19 13:55:45 +03:00
|
|
|
|
2020-06-08 03:29:51 +03:00
|
|
|
// there are mp3s files in the wild with multiple headers, two id3v2 tags etc
|
2021-11-05 17:04:26 +03:00
|
|
|
d.FieldArray("headers", func(d *decode.D) {
|
2020-06-08 03:29:51 +03:00
|
|
|
for d.NotEnd() {
|
2023-01-25 17:27:31 +03:00
|
|
|
headerStart := d.Pos()
|
2021-11-30 17:29:01 +03:00
|
|
|
if dv, _, _ := d.TryFieldFormat("header", headerFormat, nil); dv == nil {
|
2020-06-08 03:29:51 +03:00
|
|
|
return
|
|
|
|
}
|
2023-01-25 17:27:31 +03:00
|
|
|
knownSize += d.Pos() - headerStart
|
2020-06-08 03:29:51 +03:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2021-09-14 13:54:57 +03:00
|
|
|
lastValidEnd := int64(0)
|
2020-06-08 03:29:51 +03:00
|
|
|
validFrames := 0
|
2021-09-14 13:54:57 +03:00
|
|
|
decodeFailures := 0
|
2021-11-05 17:04:26 +03:00
|
|
|
d.FieldArray("frames", func(d *decode.D) {
|
2020-06-08 03:29:51 +03:00
|
|
|
for d.NotEnd() {
|
2021-12-09 19:15:21 +03:00
|
|
|
syncLen, _, err := d.TryPeekFind(16, 8, int64(mi.MaxSyncSeek), func(v uint64) bool {
|
2021-09-07 15:05:48 +03:00
|
|
|
return (v&0b1111_1111_1110_0000 == 0b1111_1111_1110_0000 && // sync header
|
|
|
|
v&0b0000_0000_0001_1000 != 0b0000_0000_0000_1000 && // not reserved mpeg version
|
|
|
|
v&0b0000_0000_0000_0110 == 0b0000_0000_0000_0010) // layer 3
|
|
|
|
})
|
2021-11-21 14:02:40 +03:00
|
|
|
if err != nil || syncLen < 0 {
|
2020-06-08 03:29:51 +03:00
|
|
|
break
|
|
|
|
}
|
|
|
|
if syncLen > 0 {
|
|
|
|
d.SeekRel(syncLen)
|
|
|
|
}
|
|
|
|
|
2023-01-25 17:27:31 +03:00
|
|
|
frameStart := d.Pos()
|
2021-11-30 17:29:01 +03:00
|
|
|
dv, v, _ := d.TryFieldFormat("frame", mp3Frame, nil)
|
2021-11-19 13:55:45 +03:00
|
|
|
if dv == nil {
|
2021-09-14 13:54:57 +03:00
|
|
|
decodeFailures++
|
|
|
|
d.SeekRel(8)
|
|
|
|
continue
|
2020-06-08 03:29:51 +03:00
|
|
|
}
|
2021-11-19 13:55:45 +03:00
|
|
|
mfo, ok := v.(format.MP3FrameOut)
|
|
|
|
if !ok {
|
|
|
|
panic(fmt.Sprintf("expected MP3FrameOut got %#+v", v))
|
|
|
|
}
|
2023-01-25 17:27:31 +03:00
|
|
|
knownSize += d.Pos() - frameStart
|
2021-11-19 13:55:45 +03:00
|
|
|
uniqueHeaderConfigs[headerConfig{
|
|
|
|
MPEGVersion: mfo.MPEGVersion,
|
|
|
|
ProtectionAbsent: mfo.ProtectionAbsent,
|
|
|
|
SampleRate: mfo.SampleRate,
|
|
|
|
ChannelsIndex: mfo.ChannelsIndex,
|
|
|
|
ChannelModeIndex: mfo.ChannelModeIndex,
|
|
|
|
}] = struct{}{}
|
|
|
|
|
2021-09-14 13:54:57 +03:00
|
|
|
lastValidEnd = d.Pos()
|
2020-06-08 03:29:51 +03:00
|
|
|
validFrames++
|
2021-11-19 13:55:45 +03:00
|
|
|
|
2021-12-09 19:15:21 +03:00
|
|
|
if len(uniqueHeaderConfigs) >= mi.MaxUniqueHeaderConfigs {
|
2021-11-19 13:55:45 +03:00
|
|
|
d.Errorf("too many unique header configurations")
|
|
|
|
}
|
2020-06-08 03:29:51 +03:00
|
|
|
}
|
|
|
|
})
|
2021-11-19 13:55:45 +03:00
|
|
|
|
2021-09-14 13:54:57 +03:00
|
|
|
if validFrames == 0 || (validFrames < 2 && decodeFailures > 0) {
|
2021-11-17 18:26:13 +03:00
|
|
|
d.Errorf("no frames found")
|
2020-06-08 03:29:51 +03:00
|
|
|
}
|
|
|
|
|
2021-09-14 13:54:57 +03:00
|
|
|
d.SeekAbs(lastValidEnd)
|
|
|
|
|
2021-11-05 17:04:26 +03:00
|
|
|
d.FieldArray("footers", func(d *decode.D) {
|
2020-06-08 03:29:51 +03:00
|
|
|
for d.NotEnd() {
|
2023-01-25 17:27:31 +03:00
|
|
|
footerStart := d.Pos()
|
2021-11-30 17:29:01 +03:00
|
|
|
if dv, _, _ := d.TryFieldFormat("footer", footerFormat, nil); dv == nil {
|
2020-06-08 03:29:51 +03:00
|
|
|
return
|
|
|
|
}
|
2023-01-25 17:27:31 +03:00
|
|
|
knownSize += d.Pos() - footerStart
|
2020-06-08 03:29:51 +03:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2023-01-25 17:27:31 +03:00
|
|
|
unknownPercent := int(float64((d.Len() - knownSize)) / float64(d.Len()) * 100.0)
|
|
|
|
if unknownPercent > mi.MaxUnknown {
|
|
|
|
d.Errorf(fmt.Sprintf("exceeds max precent unknown bits, %d > %d", unknownPercent, mi.MaxUnknown))
|
|
|
|
}
|
|
|
|
|
2020-06-08 03:29:51 +03:00
|
|
|
return nil
|
|
|
|
}
|