├── .gitignore ├── 1000hz.mp3 ├── 440hz.mp3 ├── LICENSE ├── README.md ├── frames.go ├── header.go ├── id3.go ├── id3_test.go ├── length.go ├── length_test.go ├── slice.go ├── slice_test.go ├── splice.go ├── splice_test.go ├── stripped.go ├── stripped_test.go └── xing.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /1000hz.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badgerodon/mp3/75602a2d154cb48ca2054e8979b9e1a4a0569c1e/1000hz.mp3 -------------------------------------------------------------------------------- /440hz.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badgerodon/mp3/75602a2d154cb48ca2054e8979b9e1a4a0569c1e/440hz.mp3 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 badgerodon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mp3 2 | === 3 | 4 | Go MP3 Library 5 | -------------------------------------------------------------------------------- /frames.go: -------------------------------------------------------------------------------- 1 | package mp3 2 | 3 | import ( 4 | "fmt" 5 | "github.com/badgerodon/ioutil" 6 | "io" 7 | ) 8 | 9 | type ( 10 | Frames struct { 11 | src *ioutil.SectionReader 12 | offset, count int64 13 | header FrameHeader 14 | err error 15 | } 16 | ) 17 | 18 | func GetFrames(src io.ReadSeeker) (*Frames, error) { 19 | stripped, err := Stripped(src) 20 | if err != nil { 21 | return nil, fmt.Errorf("failed to strip src: %v", err) 22 | } 23 | return &Frames{ 24 | src: stripped, 25 | offset: stripped.Offset(), 26 | }, nil 27 | } 28 | 29 | func (this *Frames) Next() bool { 30 | var err error 31 | if this.count > 0 { 32 | // skip to next frame 33 | this.offset, err = this.src.Seek(this.header.Size-4, 1) 34 | } else { 35 | this.offset, err = this.src.Seek(0, 0) 36 | } 37 | if err != nil { 38 | this.err = fmt.Errorf("premature end of frame: %v", err) 39 | return false 40 | } 41 | 42 | bs := make([]byte, 4) 43 | _, err = io.ReadAtLeast(this.src, bs, 4) 44 | if err != nil { 45 | this.err = err 46 | return false 47 | } 48 | err = this.header.Parse(bs) 49 | if err != nil { 50 | this.err = err 51 | return false 52 | } 53 | this.count++ 54 | return true 55 | } 56 | 57 | func (this *Frames) Header() *FrameHeader { 58 | return &this.header 59 | } 60 | 61 | func (this *Frames) Offset() int64 { 62 | return this.offset + this.src.Offset() 63 | } 64 | 65 | func (this *Frames) Error() error { 66 | if this.err == io.EOF { 67 | return nil 68 | } 69 | return this.err 70 | } 71 | -------------------------------------------------------------------------------- /header.go: -------------------------------------------------------------------------------- 1 | package mp3 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type ( 9 | Version byte 10 | Layer byte 11 | ChannelMode byte 12 | Emphasis byte 13 | FrameHeader struct { 14 | Version Version 15 | Layer Layer 16 | Protection bool 17 | Bitrate int 18 | SampleRate int 19 | Pad bool 20 | Private bool 21 | ChannelMode ChannelMode 22 | IntensityStereo bool 23 | MSStereo bool 24 | CopyRight bool 25 | Original bool 26 | Emphasis Emphasis 27 | 28 | Size int64 29 | Samples int 30 | Duration time.Duration 31 | } 32 | ) 33 | 34 | const ( 35 | MPEG25 Version = iota 36 | MPEGReserved 37 | MPEG2 38 | MPEG1 39 | ) 40 | 41 | const ( 42 | LayerReserved Layer = iota 43 | Layer3 44 | Layer2 45 | Layer1 46 | ) 47 | 48 | const ( 49 | EmphNone Emphasis = iota 50 | Emph5015 51 | EmphReserved 52 | EmphCCITJ17 53 | ) 54 | 55 | const ( 56 | Stereo ChannelMode = iota 57 | JointStereo 58 | DualChannel 59 | SingleChannel 60 | ) 61 | 62 | var ( 63 | bitrates = map[Version]map[Layer][15]int{ 64 | MPEG1: { // MPEG 1 65 | Layer1: {0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448}, // Layer1 66 | Layer2: {0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384}, // Layer2 67 | Layer3: {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320}, // Layer3 68 | }, 69 | MPEG2: { // MPEG 2, 2.5 70 | Layer1: {0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256}, // Layer1 71 | Layer2: {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}, // Layer2 72 | Layer3: {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}, // Layer3 73 | }, 74 | } 75 | sampleRates = map[Version][3]int{ 76 | MPEG1: {44100, 48000, 32000}, 77 | MPEG2: {22050, 24000, 16000}, 78 | MPEG25: {11025, 12000, 8000}, 79 | MPEGReserved: {0, 0, 0}, 80 | } 81 | samplesPerFrame = map[Version]map[Layer]int{ 82 | MPEG1: { 83 | Layer1: 384, 84 | Layer2: 1152, 85 | Layer3: 1152, 86 | }, 87 | MPEG2: { 88 | Layer1: 384, 89 | Layer2: 1152, 90 | Layer3: 576, 91 | }, 92 | } 93 | slotSize = map[Layer]int{ 94 | LayerReserved: 0, 95 | Layer3: 1, 96 | Layer2: 1, 97 | Layer1: 4, 98 | } 99 | ) 100 | 101 | func init() { 102 | bitrates[MPEG25] = bitrates[MPEG2] 103 | samplesPerFrame[MPEG25] = samplesPerFrame[MPEG2] 104 | } 105 | 106 | func (this *FrameHeader) Parse(bs []byte) error { 107 | this.Size = 0 108 | this.Samples = 0 109 | this.Duration = 0 110 | 111 | if len(bs) < 4 { 112 | return fmt.Errorf("not enough bytes") 113 | } 114 | if bs[0] != 0xFF || (bs[1]&0xE0) != 0xE0 { 115 | return fmt.Errorf("missing sync word, got: %x, %x", bs[0], bs[1]) 116 | } 117 | this.Version = Version((bs[1] >> 3) & 0x03) 118 | if this.Version == MPEGReserved { 119 | return fmt.Errorf("reserved mpeg version") 120 | } 121 | 122 | this.Layer = Layer(((bs[1] >> 1) & 0x03)) 123 | if this.Layer == LayerReserved { 124 | return fmt.Errorf("reserved layer") 125 | } 126 | 127 | this.Protection = (bs[1] & 0x01) != 0x01 128 | 129 | bitrateIdx := (bs[2] >> 4) & 0x0F 130 | if bitrateIdx == 0x0F { 131 | return fmt.Errorf("invalid bitrate: %v", bitrateIdx) 132 | } 133 | this.Bitrate = bitrates[this.Version][this.Layer][bitrateIdx] * 1000 134 | if this.Bitrate == 0 { 135 | return fmt.Errorf("invalid bitrate: %v", bitrateIdx) 136 | } 137 | 138 | sampleRateIdx := (bs[2] >> 2) & 0x03 139 | if sampleRateIdx == 0x03 { 140 | return fmt.Errorf("invalid sample rate: %v", sampleRateIdx) 141 | } 142 | this.SampleRate = sampleRates[this.Version][sampleRateIdx] 143 | 144 | this.Pad = ((bs[2] >> 1) & 0x01) == 0x01 145 | 146 | this.Private = (bs[2] & 0x01) == 0x01 147 | 148 | this.ChannelMode = ChannelMode(bs[3]>>6) & 0x03 149 | 150 | // todo: mode extension 151 | 152 | this.CopyRight = (bs[3]>>3)&0x01 == 0x01 153 | 154 | this.Original = (bs[3]>>2)&0x01 == 0x01 155 | 156 | this.Emphasis = Emphasis(bs[3] & 0x03) 157 | if this.Emphasis == EmphReserved { 158 | return fmt.Errorf("reserved emphasis") 159 | } 160 | 161 | this.Size = this.size() 162 | this.Samples = this.samples() 163 | this.Duration = this.duration() 164 | 165 | return nil 166 | } 167 | 168 | func (this *FrameHeader) samples() int { 169 | return samplesPerFrame[this.Version][this.Layer] 170 | } 171 | 172 | func (this *FrameHeader) size() int64 { 173 | bps := float64(this.samples()) / 8 174 | fsize := (bps * float64(this.Bitrate)) / float64(this.SampleRate) 175 | if this.Pad { 176 | fsize += float64(slotSize[this.Layer]) 177 | } 178 | return int64(fsize) 179 | } 180 | 181 | func (this *FrameHeader) duration() time.Duration { 182 | ms := (1000 / float64(this.SampleRate)) * float64(this.samples()) 183 | return time.Duration(time.Duration(float64(time.Millisecond) * ms)) 184 | } 185 | -------------------------------------------------------------------------------- /id3.go: -------------------------------------------------------------------------------- 1 | package mp3 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type ( 8 | ID3V2Header struct { 9 | Version struct { 10 | Major, Revision byte 11 | } 12 | Flags byte 13 | Size int64 14 | } 15 | ) 16 | 17 | func (this *ID3V2Header) Parse(bs []byte) error { 18 | if len(bs) < 10 { 19 | return fmt.Errorf("Expected at least 10 bytes, got: %v", len(bs)) 20 | } 21 | if bs[0] != 'I' || bs[1] != 'D' || bs[2] != '3' { 22 | return fmt.Errorf("Expected ID3 head to start with ID3, got: %v", bs[:3]) 23 | } 24 | 25 | this.Version.Major = bs[3] 26 | this.Version.Revision = bs[4] 27 | this.Flags = bs[5] 28 | if bs[6] >= 0x80 || bs[7] >= 0x80 || bs[8] >= 0x80 || bs[9] >= 0x80 { 29 | return fmt.Errorf("Invalid size, got: %v", bs[6:]) 30 | } 31 | 32 | this.Size = int64(bs[9]) + 33 | int64(uint(bs[8])<<7) + 34 | int64(uint(bs[7])<<14) + 35 | int64(uint(bs[6])<<21) + 36 | 10 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /id3_test.go: -------------------------------------------------------------------------------- 1 | package mp3 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestID3(t *testing.T) { 8 | valid := [][]byte{ 9 | {'I', 'D', '3', 0, 0, 0, 0, 0, 0, 0, 0}, 10 | } 11 | invalid := [][]byte{ 12 | nil, 13 | {'I', 'D', '2', 0, 0, 0, 0, 0, 0, 0, 0}, 14 | } 15 | 16 | var id3 ID3V2Header 17 | 18 | for _, test := range valid { 19 | if id3.Parse(test) != nil { 20 | t.Errorf("Expected valid ID3 tag for: %v", test) 21 | } 22 | } 23 | 24 | for _, test := range invalid { 25 | if id3.Parse(test) == nil { 26 | t.Errorf("Expected invalid ID3 tag for: %v", test) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /length.go: -------------------------------------------------------------------------------- 1 | package mp3 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "time" 7 | ) 8 | 9 | func Length(src io.ReadSeeker) (time.Duration, error) { 10 | frames, err := GetFrames(src) 11 | if err != nil { 12 | return 0, fmt.Errorf("failed to parse frames: %v", err) 13 | } 14 | 15 | duration := time.Duration(0) 16 | 17 | for frames.Next() { 18 | duration += frames.Header().Duration 19 | } 20 | 21 | return duration, frames.Error() 22 | } 23 | -------------------------------------------------------------------------------- /length_test.go: -------------------------------------------------------------------------------- 1 | package mp3 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestLength(t *testing.T) { 10 | f, err := os.Open("1000hz.mp3") 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | defer f.Close() 15 | 16 | duration, err := Length(f) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | if duration != 5067754912 { 21 | t.Errorf("Expected length to return %v, got %v", time.Duration(5067754912), duration) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /slice.go: -------------------------------------------------------------------------------- 1 | package mp3 2 | 3 | import ( 4 | //"fmt" 5 | "github.com/badgerodon/ioutil" 6 | "io" 7 | "time" 8 | ) 9 | 10 | func Slice(src io.ReadSeeker, cutPoints ...time.Duration) ([]io.ReadSeeker, error) { 11 | pieces := make([]io.ReadSeeker, 0, len(cutPoints)+1) 12 | 13 | stripped, err := Stripped(src) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | start := stripped.Offset() 19 | end := stripped.Offset() + stripped.Length() 20 | 21 | frames, err := GetFrames(src) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | var elapsed time.Duration 27 | lastOffset := start 28 | for frames.Next() { 29 | if len(cutPoints) == 0 { 30 | break 31 | } 32 | 33 | elapsed += frames.Header().Duration 34 | 35 | if cutPoints[0] <= elapsed { 36 | piece := ioutil.NewSectionReader(src, lastOffset, frames.Offset()+frames.Header().Size-lastOffset) 37 | pieces = append(pieces, piece) 38 | lastOffset = frames.Offset() + frames.Header().Size 39 | cutPoints = cutPoints[1:] 40 | } 41 | } 42 | 43 | pieces = append(pieces, ioutil.NewSectionReader(src, lastOffset, end-lastOffset)) 44 | 45 | return pieces, frames.Error() 46 | } 47 | -------------------------------------------------------------------------------- /slice_test.go: -------------------------------------------------------------------------------- 1 | package mp3 2 | 3 | import ( 4 | "github.com/badgerodon/ioutil" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestSlice(t *testing.T) { 11 | nearlyEqual := func(a, b time.Duration) bool { 12 | diff := a - b 13 | if diff < 0 { 14 | diff = -diff 15 | } 16 | return diff < 100*time.Millisecond 17 | } 18 | 19 | f, err := os.Open("440hz.mp3") 20 | if err != nil { 21 | t.Fatalf("Error opening sample file: %v", err) 22 | } 23 | defer f.Close() 24 | 25 | slices, err := Slice(f, time.Second*1, time.Second*2) 26 | if err != nil { 27 | t.Fatalf("Expected no error, got: %v", err) 28 | } 29 | if len(slices) != 3 { 30 | t.Fatalf("Expected 3 slices, got: %v", err) 31 | } 32 | len1, err := Length(slices[0]) 33 | if err != nil { 34 | t.Errorf("Expected no error, got: %v", err) 35 | } 36 | if !nearlyEqual(len1, time.Second) { 37 | t.Errorf("Expected %v to be %v, got %v", slices[0], time.Second, len1) 38 | } 39 | len2, err := Length(slices[1]) 40 | if err != nil { 41 | t.Errorf("Expected no error for %v, got: %v", slices[1], err) 42 | } 43 | if !nearlyEqual(len2, time.Second) { 44 | t.Errorf("Expected %v to be %v, got %v", slices[1], time.Second, len2) 45 | } 46 | len3, err := Length(slices[2]) 47 | if err != nil { 48 | t.Errorf("Expected no error for %v, got: %v", slices[2], err) 49 | } 50 | if !nearlyEqual(len3, 3*time.Second) { 51 | t.Errorf("Expected the third slice to be %v, got %v", 3*time.Second, len3) 52 | } 53 | 54 | } 55 | 56 | func TestSlicedAndConcatenated(t *testing.T) { 57 | f, err := os.Open("440hz.mp3") 58 | if err != nil { 59 | t.Fatalf("Error opening sample file: %v", err) 60 | } 61 | defer f.Close() 62 | 63 | slices, err := Slice(f, time.Second, 2*time.Second, 3*time.Second) 64 | if err != nil { 65 | t.Fatalf("Expected no error, got: %v", err) 66 | } 67 | 68 | concatenated := ioutil.NewMultiReadSeeker(slices...) 69 | 70 | l1, err := Length(f) 71 | if err != nil { 72 | t.Fatalf("Expected no error, got: %v", err) 73 | } 74 | l2, err := Length(concatenated) 75 | if err != nil { 76 | t.Fatalf("Expected no error, got: %v", err) 77 | } 78 | if l1 != l2 { 79 | t.Errorf("Expected concatenated (%v) to be the same length as original (%v)", l2, l1) 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /splice.go: -------------------------------------------------------------------------------- 1 | package mp3 2 | 3 | import ( 4 | "fmt" 5 | "github.com/badgerodon/ioutil" 6 | "io" 7 | "sort" 8 | "time" 9 | ) 10 | 11 | type durationSorter []time.Duration 12 | 13 | func (this durationSorter) Len() int { 14 | return len(this) 15 | } 16 | func (this durationSorter) Swap(i, j int) { 17 | this[i], this[j] = this[j], this[i] 18 | } 19 | func (this durationSorter) Less(i, j int) bool { 20 | return this[i] < this[j] 21 | } 22 | 23 | // Take a source MP3 and insert all the splice members into it (at the specified durations) 24 | func Splice(src io.ReadSeeker, splice map[time.Duration]io.ReadSeeker) (*ioutil.MultiReadSeeker, error) { 25 | // Get the times 26 | spliceTimes := []time.Duration{} 27 | for k, _ := range splice { 28 | spliceTimes = append(spliceTimes, k) 29 | } 30 | sort.Sort(durationSorter(spliceTimes)) 31 | 32 | // Slice up the src into len(splice)+1 pieces 33 | sliced, err := Slice(src, spliceTimes...) 34 | if err != nil { 35 | return nil, fmt.Errorf("error slicing src: %v", err) 36 | } 37 | 38 | // Insert splice members between the slices 39 | pieces := []io.ReadSeeker{sliced[0]} 40 | for i := 1; i < len(sliced); i++ { 41 | stripped, err := Stripped(splice[spliceTimes[i-1]]) 42 | if err != nil { 43 | return nil, err 44 | } 45 | pieces = append(pieces, stripped, sliced[i]) 46 | } 47 | 48 | // Treat all the pieces as one big ReadSeeker 49 | return ioutil.NewMultiReadSeeker(pieces...), nil 50 | } 51 | -------------------------------------------------------------------------------- /splice_test.go: -------------------------------------------------------------------------------- 1 | package mp3 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSplice(t *testing.T) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /stripped.go: -------------------------------------------------------------------------------- 1 | package mp3 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/badgerodon/ioutil" 7 | "io" 8 | ) 9 | 10 | func getFirstFrameOffset(src io.ReadSeeker) (int64, error) { 11 | var id3v2 ID3V2Header 12 | _, err := src.Seek(0, 0) 13 | if err != nil { 14 | return 0, err 15 | } 16 | 17 | bs := make([]byte, 10) 18 | 19 | _, err = io.ReadAtLeast(src, bs, 10) 20 | if err != nil { 21 | return 0, fmt.Errorf("Failed to read first 10 bytes: %v", err) 22 | } 23 | 24 | err = id3v2.Parse(bs) 25 | if err == nil { 26 | return id3v2.Size, nil 27 | } 28 | 29 | return 0, nil 30 | } 31 | 32 | // Skips both the ID3V2 tags and optional VBR headers 33 | func getFirstRealFrameOffset(src io.ReadSeeker) (int64, error) { 34 | var hdr FrameHeader 35 | var xing XingHeader 36 | 37 | off, err := getFirstFrameOffset(src) 38 | if err != nil { 39 | return 0, err 40 | } 41 | 42 | _, err = src.Seek(off, 0) 43 | if err != nil { 44 | return 0, err 45 | } 46 | 47 | bs := make([]byte, 8192) 48 | 49 | _, err = io.ReadAtLeast(src, bs, 4) 50 | if err != nil { 51 | return 0, err 52 | } 53 | 54 | err = hdr.Parse(bs) 55 | if err != nil { 56 | return 0, err 57 | } 58 | 59 | if xing.Parse(bs[:int(hdr.Size)]) { 60 | return off + hdr.Size, nil 61 | } 62 | 63 | return off, nil 64 | } 65 | 66 | func getLastFrameEnd(src io.ReadSeeker) (int64, error) { 67 | end, err := src.Seek(-128, 2) 68 | if err != nil { 69 | return 0, nil 70 | } 71 | 72 | bs := make([]byte, 3) 73 | _, err = io.ReadAtLeast(src, bs, 3) 74 | if err != nil { 75 | return 0, err 76 | } 77 | if !bytes.Equal(bs, []byte("TAG")) { 78 | end += 128 79 | } 80 | return end, nil 81 | } 82 | 83 | func Stripped(src io.ReadSeeker) (*ioutil.SectionReader, error) { 84 | o1, err := getFirstRealFrameOffset(src) 85 | if err != nil { 86 | return nil, err 87 | } 88 | o2, err := getLastFrameEnd(src) 89 | if err != nil { 90 | return nil, err 91 | } 92 | return ioutil.NewSectionReader(src, o1, o2-o1), nil 93 | } 94 | -------------------------------------------------------------------------------- /stripped_test.go: -------------------------------------------------------------------------------- 1 | package mp3 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestStripped(t *testing.T) { 10 | f, err := os.Open("440hz.mp3") 11 | if err != nil { 12 | t.Fatalf("Error opening sample: %v", err) 13 | } 14 | defer f.Close() 15 | 16 | off, err := getFirstFrameOffset(f) 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | 21 | if off != 33 { 22 | t.Errorf("Expected offset to be %v, got %v", 33, off) 23 | } 24 | 25 | off, err = getFirstRealFrameOffset(f) 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | 30 | if off != 33 { 31 | t.Errorf("Expected offset to be %v, got %v", 33, off) 32 | } 33 | 34 | end, err := getLastFrameEnd(f) 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | 39 | if end != 30439 { 40 | t.Errorf("Expected offset to be %v, got %v", 30439, end) 41 | } 42 | 43 | limited, err := Stripped(f) 44 | if err != nil { 45 | t.Error(err) 46 | } 47 | bs, err := ioutil.ReadAll(limited) 48 | if err != nil { 49 | t.Error(err) 50 | } 51 | if len(bs) != 30406 { 52 | t.Errorf("Expected %v bytes, got %v", 30406, len(bs)) 53 | } 54 | 55 | limited, err = Stripped(limited) 56 | if err != nil { 57 | t.Error(err) 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /xing.go: -------------------------------------------------------------------------------- 1 | package mp3 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | type XingHeader struct { 9 | Frames, Bytes, Quality int 10 | } 11 | 12 | // Parse an Xing header from the first frame of an mp3 13 | func (this *XingHeader) Parse(src []byte) bool { 14 | if len(src) == 0 { 15 | return false 16 | } 17 | 18 | // parse header id 19 | idx := bytes.Index(src, []byte("Xing")) 20 | if idx < 0 { 21 | idx = bytes.Index(src, []byte("Info")) 22 | } 23 | if idx < 0 { 24 | return false 25 | } 26 | 27 | src = src[idx+4:] 28 | 29 | flags := binary.BigEndian.Uint32(src[:4]) 30 | src = src[4:] 31 | // Frames 32 | if flags&1 == 1 { 33 | this.Frames = int(binary.BigEndian.Uint32(src[:4])) 34 | src = src[4:] 35 | } 36 | // Bytes 37 | if flags&2 == 2 { 38 | this.Bytes = int(binary.BigEndian.Uint32(src[:4])) 39 | src = src[4:] 40 | } 41 | // TOC 42 | if flags&4 == 4 { 43 | src = src[4:] 44 | } 45 | // Quality 46 | if flags&8 == 8 { 47 | this.Quality = int(binary.BigEndian.Uint32(src[:4])) 48 | } 49 | 50 | return true 51 | } 52 | --------------------------------------------------------------------------------