github.com/derat/nup@v0.0.0-20230418113745-15592ba7c620/cmd/nup/debug/mpeg.go (about) 1 // Copyright 2022 Daniel Erat. 2 // All rights reserved. 3 4 package debug 5 6 import ( 7 "bytes" 8 "errors" 9 "fmt" 10 "os" 11 "time" 12 13 "github.com/derat/mpeg" 14 "github.com/derat/taglib-go/taglib" 15 ) 16 17 // mpegInfo contains debugging info about an MP3 file. 18 type mpegInfo struct { 19 size int64 // entire file 20 header int64 // ID3v2 header size 21 footer int64 // ID3v1 footer size 22 sha1 string // SHA1 of data between header and footer 23 vbr bool // bitrate is variable (as opposed to constant) 24 avgKbitRate float64 // averaged across audio frames 25 sampleRate int // from first audio frame 26 samplesPerFrame int // from first audio frame 27 xingFrames int // number of frames from Xing header 28 xingBytes int64 // audio data size from Xing header 29 xingDur time.Duration // audio duration from Xing header (or guessed as CBR) 30 xingEnc string // human-readable encoder version and settings 31 actualFrames int // actual frame count 32 actualBytes int64 // actual audio data size 33 actualDur time.Duration // actual duration 34 emptyFrame int // first empty frame at end of file 35 emptyOffset int64 // offset of emptyFrame from start of file 36 emptyTime time.Duration // time of emptyFrame 37 skipped []skipInfo // skipped regions 38 } 39 40 // skipInfo contains details about an invalid region of an MP3 file that was skipped. 41 type skipInfo struct { 42 offset, size int64 // in bytes 43 err error // first error 44 } 45 46 // getMPEGInfo returns debug information about the MP3 file at p. 47 func getMPEGInfo(p string) (*mpegInfo, error) { 48 f, err := os.Open(p) 49 if err != nil { 50 return nil, err 51 } 52 defer f.Close() 53 54 fi, err := f.Stat() 55 if err != nil { 56 return nil, err 57 } 58 59 info := mpegInfo{size: fi.Size(), emptyFrame: -1} 60 if tag, err := mpeg.ReadID3v1Footer(f, fi); err == nil && tag != nil { 61 info.footer = mpeg.ID3v1Length 62 } 63 if tag, err := taglib.Decode(f, fi.Size()); err == nil { 64 info.header = int64(tag.TagSize()) 65 } 66 if info.sha1, err = mpeg.ComputeAudioSHA1(f, fi, info.header, info.footer); err != nil { 67 return &info, fmt.Errorf("failed computing SHA1: %v", err) 68 } 69 70 // Read the Xing header. 71 var vbrInfo *mpeg.VBRInfo 72 if info.xingDur, vbrInfo, err = mpeg.ComputeAudioDuration(f, fi, info.header, info.footer); err != nil { 73 return &info, fmt.Errorf("failed computing duration: %v", err) 74 } 75 if vbrInfo != nil { 76 // An "Xing" header ID (as opposed to "Info") typically indicates a VBR stream. 77 // Make this assumption so that we won't report streams that were encoded with VBR 78 // settings as CBR if all of the frames happen to have the same bitrate. 79 info.vbr = vbrInfo.ID == mpeg.XingID && (vbrInfo.Method != mpeg.CBR && vbrInfo.Method != mpeg.CBR2Pass) 80 info.xingFrames = int(vbrInfo.Frames) 81 info.xingBytes = int64(vbrInfo.Bytes) 82 if vbrInfo.Encoder != "" { 83 info.xingEnc = fmt.Sprintf("%s %s", vbrInfo.Encoder, vbrInfo.Method) 84 } 85 } 86 87 // Read all of the frames in the file. 88 off := info.header 89 var skipped, kbitRateSum int64 90 var skipErr error 91 var lastKbitRate int 92 93 addSkipped := func() { 94 if skipped == 0 { 95 return 96 } 97 98 start := off - skipped 99 100 // Diagnose common errors. 101 b := make([]byte, skipped) 102 if _, err := f.ReadAt(b, start); err == nil { 103 if bytes.HasPrefix(b, []byte("LYRICSBEGIN")) && bytes.HasSuffix(b, []byte("LYRICSEND")) { 104 skipErr = errors.New("Lyrics3v1 tag") 105 } else if bytes.HasPrefix(b, []byte("LYRICSBEGIN")) && bytes.HasSuffix(b, []byte("LYRICS200")) { 106 skipErr = errors.New("Lyrics3v2 tag") 107 } else if bytes.HasPrefix(b, []byte("TAG")) { 108 skipErr = errors.New("extra ID3v1 tag") 109 } else if bytes.Count(b, []byte{0}) == len(b) { 110 skipErr = errors.New("empty") 111 } 112 } 113 info.skipped = append(info.skipped, skipInfo{ 114 offset: start, 115 size: skipped, 116 err: skipErr, 117 }) 118 skipped = 0 119 skipErr = nil 120 } 121 122 for off < info.size-info.footer { 123 finfo, err := mpeg.ReadFrameInfo(f, off) 124 if err != nil { 125 off++ 126 skipped++ 127 if skipErr == nil { 128 skipErr = err 129 } 130 continue 131 } 132 133 addSkipped() 134 135 if isXing := info.xingBytes != 0 && info.actualFrames == 0; isXing { 136 } else { 137 // Skip the Xing frame for bitrate calculations since its bitrate sometimes 138 // differs from the audio frames in CBR files. See e.g. 139 // https://github.com/JamesHeinrich/getID3/issues/287#issuecomment-786357902. 140 if lastKbitRate > 0 && finfo.KbitRate != lastKbitRate { 141 info.vbr = true 142 } 143 lastKbitRate = finfo.KbitRate 144 kbitRateSum += int64(finfo.KbitRate) 145 146 // Get the sample rate from the first non-Xing frame we find. Getting it from the Xing 147 // frame seemed to work as well, but it feels a bit safer to skip it since I've seen 148 // different bitrates there. 149 if info.sampleRate == 0 { 150 info.sampleRate = finfo.SampleRate 151 info.samplesPerFrame = finfo.SamplesPerFrame 152 } 153 } 154 155 // Check for empty frames at the end of the file. 156 if finfo.Empty() { 157 if info.emptyFrame < 0 { 158 info.emptyFrame = info.actualFrames 159 info.emptyOffset = off 160 } 161 } else { 162 info.emptyFrame = -1 163 } 164 info.actualFrames++ 165 info.actualBytes += finfo.Size() 166 off += finfo.Size() 167 } 168 addSkipped() 169 170 // The Xing header apparently doesn't include itself in the frame count 171 // (but confusingly *does* include itself in the bytes count): 172 // https://www.mail-archive.com/mp3encoder@minnie.tuhs.org/msg02868.html 173 // https://hydrogenaud.io/index.php?topic=85690.0 174 // If it was present, adjust fields so they'll be comparable to xingFrames. 175 if info.xingFrames != 0 { 176 info.actualFrames-- 177 if info.emptyFrame > 0 { 178 info.emptyFrame-- 179 } 180 } 181 182 // Do this after the above decrement since we excluded the Xing frame from kbitRateSum. 183 if info.actualFrames > 0 { 184 info.avgKbitRate = float64(kbitRateSum) / float64(info.actualFrames) 185 } 186 187 // Compute durations. The sample rate is fixed and there's a constant number 188 // of samples per frame, so we just need the number of frames. 189 computeDur := func(frames int) time.Duration { 190 if info.sampleRate == 0 { 191 return 0 192 } 193 return time.Duration(info.samplesPerFrame*frames) * time.Second / 194 time.Duration(info.sampleRate) 195 } 196 info.actualDur = computeDur(info.actualFrames) 197 if info.emptyFrame >= 0 { 198 info.emptyTime = computeDur(info.emptyFrame) 199 } 200 201 return &info, nil 202 }