[fetch bitrate, duration & size] support for mp3

This commit is contained in:
Xavier Henner 2015-05-20 01:50:31 +02:00
parent 7a80548ea8
commit 1c6635a14e
2 changed files with 256 additions and 0 deletions

View File

@ -285,5 +285,16 @@ func ReadID3v2Tags(r io.ReadSeeker) (Metadata, error) {
if err != nil {
return nil, err
}
mp3, err := getMp3Infos(r, false)
if err != nil {
return nil, err
}
f["stream_type"] = fmt.Sprintf("MPEG %v Layer %v", mp3.Version, mp3.Layer)
f["stream_bitrate"] = fmt.Sprintf("%v kbps %v", mp3.Bitrate, mp3.Type)
f["stream_audio"] = fmt.Sprintf("%v Hz %v", mp3.Sampling, mp3.Mode)
f["stream_size"] = mp3.Size
f["stream_length"] = int(mp3.Length)
return metadataID3v2{header: h, frames: f}, nil
}

245
mp3.go Normal file
View File

@ -0,0 +1,245 @@
package tag
import (
"encoding/binary"
"errors"
"io"
"math"
)
// Some documentation :
// http://id3.org/mp3Frame
// http://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header
// the number of frames to scan in fast mode
type mp3Infos struct {
Version string
Layer string
Type string
Mode string
Bitrate int
Sampling int
Size int64
Length float64
vbr int
}
func getMp3Infos(r io.ReadSeeker, slow bool) (*mp3Infos, error) {
h := new(mp3Infos)
var err error
var nbscan, bitrateSum, frameCount int
var pos, start int64
var buf [8]byte
nbscan = 50
// skip the padding at the start
for ; buf[0] == 0; _, err = r.Read(buf[0:1]) {
if err != nil {
return nil, err
}
}
// no more padding, we are now at the start of the actual data
start, err = r.Seek(-1, 1)
if err != nil {
return nil, err
}
// we read the first frame. Maybe a xing header
j, err := r.Read(buf[:4])
if j < 4 || err != nil {
return nil, errors.New("not a MP3 file")
}
offset := h.readHeader(buf)
if offset == 5 {
return nil, errors.New("not a MP3 file")
}
if !(buf[0] == 255 && buf[1] >= 224) {
return nil, errors.New("not a MP3 file")
}
_, err = r.Seek(xingoffset(h.Version, h.Mode), 1)
if err != nil {
return nil, err
}
_, err = r.Read(buf[:8])
if err != nil {
return nil, err
}
if !slow && (string(buf[:4]) == "Xing" || string(buf[:4]) == "Info") {
flags := buf[7]
if (1&flags != 0) && (2&flags != 0) {
var frames, size uint32
binary.Read(r, binary.BigEndian, &frames)
binary.Read(r, binary.BigEndian, &size)
h.Length = float64(frames) * samplePerFrame(h.Version, h.Layer) / float64(h.Sampling)
h.Size = int64(size)
bitrate := getNearestBitrate(float64(h.Size/125)/h.Length, h.Version, h.Layer)
if bitrate != h.Bitrate {
h.Bitrate = bitrate
h.Type = "VBR"
}
return h, nil
}
}
//TODO support VBRI Header and LAME extension
// go to the next frame
_, err = r.Seek(start+offset, 0)
for i := 0; err != io.EOF && (slow || frameCount < nbscan); {
i, err = r.Read(buf[:4])
if i < 4 {
break
}
pos += int64(i)
// looking for the synchronization bits
switch {
case (buf[0] == 255) && (buf[1] >= 224):
// found a valid mp3 frame. we read the header to know where the
// next one is
pos, _ = r.Seek(h.readHeader(buf)-4, 1)
bitrateSum += h.Bitrate
frameCount++
if h.vbr > 2 {
nbscan = 100
}
break
case string(buf[:3]) == "TAG":
pos, _ = r.Seek(128-4, 1) // id3v1 tag, bypass it
break
default:
r.Seek(-3, 1) // looking for the next header
}
}
// Extrapolate the total length base on the nbscan readHeaders
if err == io.EOF {
h.Size = pos
} else {
end, err := r.Seek(0, 2)
if err != nil {
return h, err
}
h.Length = h.Length * float64(end-int64(start)) / float64(pos-int64(start))
h.Size = end
}
// For VBR, choose the closest match
if frameCount > 1 || h.Type == "VBR" {
h.Bitrate = getNearestBitrate(float64(bitrateSum/frameCount), h.Version, h.Layer)
}
return h, nil
}
func getNearestBitrate(s float64, v string, l string) int {
diff := s
result := int(s)
for _, v := range mp3Bitrate[v+l] {
if math.Abs(float64(v)-s) < diff {
result = v
diff = math.Abs(float64(v) - s)
}
}
return result
}
func (h *mp3Infos) readHeader(buf [8]byte) int64 {
v := buf[1] & 24 >> 3
l := buf[1] & 6 >> 1
b := buf[2] & 240 >> 4
s := buf[2] & 12 >> 2
c := buf[3] & 192 >> 6
// if the values are off, try 1 byte after
if l == 0 || b == 15 || v == 1 || b == 0 || s == 3 {
return 11
}
if h.Version == "" {
h.Version = mp3Version[v]
h.Layer = mp3Layer[l]
h.Sampling = mp3Sampling[mp3Version[v]][s]
h.Mode = mp3Channel[c]
h.Type = "CBR"
}
bitrate := mp3Bitrate[mp3Version[v]+mp3Layer[l]][b]
mult := frameLengthMult[mp3Version[v]+mp3Layer[l]]
switch {
case h.vbr > 2:
h.Type = "VBR"
case bitrate != h.Bitrate:
h.vbr++
}
h.Bitrate = bitrate
samples := samplePerFrame(mp3Version[v], mp3Layer[l])
h.Length += samples / float64(h.Sampling)
return int64(mult * bitrate * 1000 / h.Sampling)
}
func xingoffset(v string, m string) int64 {
switch {
case v == "2" && m == "mono":
return 9
case v == "1" && m != "mono":
return 32
default:
return 17
}
}
func samplePerFrame(v string, l string) float64 {
switch {
case v == "1" && l == "I":
return 384
case (v == "2" || v == "2.5") && l == "III":
return 576
}
return 1152
}
// constants for deconding frames
var (
mp3Version = [4]string{"2.5", "x", "2", "1"}
mp3Layer = [4]string{"r", "III", "II", "I"}
mp3Bitrate = map[string][16]int{
"1I": {0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448},
"1II": {0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384},
"1III": {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320},
"2I": {0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256},
"2II": {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160},
"2III": {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160},
"2.5I": {0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256},
"2.5II": {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160},
"2.5III": {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160},
}
mp3Sampling = map[string][4]int{
"1": {44100, 48000, 32000, 0},
"2": {22050, 24000, 16000, 0},
"2.5": {11025, 12000, 8000, 0},
}
mp3Channel = [4]string{"Stereo", "Join Stereo", "Dual", "Mono"}
frameLengthMult = map[string]int{
"1I": 48,
"1II": 144,
"1III": 144,
"2I": 24,
"2II": 144,
"2III": 72,
"2.5I": 24,
"2.5II": 72,
"2.5III": 144,
}
)