Skip to content

Commit 481551d

Browse files
committed
feat: decode Vorbis audio tracks
1 parent 271b7d5 commit 481551d

8 files changed

Lines changed: 714 additions & 12 deletions

File tree

extract_audio.go

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ package matroska
22

33
import (
44
"fmt"
5+
"github.com/coding-socks/matroska/internal/vorbis"
56
"io"
7+
"math/rand/v2"
68
)
79

810
func extractTrackAudio(w io.Writer, s *Scanner, t TrackEntry) error {
911
switch t.CodecID {
1012
case AudioCodecMP2, AudioCodecMP3:
1113
return extractTrackMPEG(w, s, t)
14+
case AudioCodecVORBIS:
15+
return extractTrackVORBIS(w, s, t)
1216
}
1317
return fmt.Errorf("matroska: unknown audio codec %s", t.CodecID)
1418
}
@@ -25,23 +29,120 @@ func extractTrackMPEG(w io.Writer, s *Scanner, t TrackEntry) error {
2529
if err != nil {
2630
return fmt.Errorf("matroska: could not create block struct: %w", err)
2731
}
28-
if block.TrackNumber() == t.TrackNumber {
29-
if _, err := io.Copy(w, block.Data()); err != nil {
30-
return err
31-
}
32+
if block.TrackNumber() != t.TrackNumber {
33+
continue
34+
}
35+
if _, err := io.Copy(w, block.Data()); err != nil {
36+
return err
3237
}
3338
}
3439
for i := range c.BlockGroup {
3540
block, err := ReadBlock(c.BlockGroup[i].Block, c.Timestamp)
3641
if err != nil {
3742
return fmt.Errorf("matroska: could not create block struct: %w", err)
3843
}
39-
if block.TrackNumber() == t.TrackNumber {
40-
if _, err := io.Copy(w, block.Data()); err != nil {
44+
if block.TrackNumber() != t.TrackNumber {
45+
continue
46+
}
47+
if _, err := io.Copy(w, block.Data()); err != nil {
48+
return err
49+
}
50+
}
51+
}
52+
return nil
53+
}
54+
55+
// extractTrackVORBIS is based on the Vorbis I specification created by the Xiph.Org Foundation.
56+
// See: https://xiph.org/vorbis/doc/Vorbis_I_spec.pdf
57+
func extractTrackVORBIS(w io.Writer, s *Scanner, track TrackEntry) error {
58+
serialNum := rand.Int32()
59+
60+
vw := vorbis.NewWriter(w, serialNum)
61+
62+
codecPrivate := *track.CodecPrivate
63+
frames := Frames(LacingFlagXiph, codecPrivate)
64+
if len(frames) != 3 {
65+
return fmt.Errorf("matroska: Vorbis audio track requires 3 header pages, got %d", len(frames))
66+
}
67+
68+
ih := frames[0]
69+
if err := vw.WriteIdentHeader(ih); err != nil {
70+
return err
71+
}
72+
iheader, err := vorbis.ParseIdentificationHeader([30]byte(ih))
73+
if err != nil {
74+
return err
75+
}
76+
blockSizes := []uint16{
77+
iheader.Blocksize0,
78+
iheader.Blocksize1,
79+
}
80+
cheader, err := vorbis.ParseCommentHeader(frames[1])
81+
if err != nil {
82+
return err
83+
}
84+
_ = cheader
85+
86+
if err := vw.WriteHeaders(frames[1], frames[2]); err != nil {
87+
return err
88+
}
89+
90+
var (
91+
prevBlockSize uint64 = 0
92+
granpos uint64 = 0
93+
94+
prevFrame []byte //
95+
)
96+
frames = frames[:0] // clear value
97+
98+
for s.Next() {
99+
c := s.Cluster()
100+
if (len(c.SimpleBlock) + len(c.BlockGroup)) == 0 {
101+
continue
102+
}
103+
104+
for i := range c.SimpleBlock {
105+
block, err := ReadSimpleBlock(c.SimpleBlock[i], c.Timestamp)
106+
if err != nil {
107+
return fmt.Errorf("matroska: could not create block struct: %w", err)
108+
}
109+
if block.TrackNumber() != track.TrackNumber {
110+
continue
111+
}
112+
frames = append(frames, block.Frames()...)
113+
}
114+
for i := range c.BlockGroup {
115+
block, err := ReadBlock(c.BlockGroup[i].Block, c.Timestamp)
116+
if err != nil {
117+
return fmt.Errorf("matroska: could not create block struct: %w", err)
118+
}
119+
if block.TrackNumber() != track.TrackNumber {
120+
continue
121+
}
122+
frames = append(frames, block.Frames()...)
123+
}
124+
125+
for _, frame := range frames {
126+
blockSize := uint64(blockSizes[(frame[0]>>1)&1])
127+
128+
if prevFrame != nil {
129+
if err := vw.Segment(prevFrame, granpos, false); err != nil {
41130
return err
42131
}
132+
133+
// We need at least two segment to calculate this
134+
granpos += (blockSize + prevBlockSize) / 4
43135
}
136+
137+
prevBlockSize = blockSize
138+
prevFrame = frame
44139
}
140+
141+
frames = frames[:0]
45142
}
46-
return nil
143+
if prevFrame == nil {
144+
return nil
145+
}
146+
// only this element can be the last.
147+
return vw.Segment(prevFrame, granpos, true)
47148
}

extract_video.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,20 @@ func extractTrackMSCOMP(w io.Writer, s *Scanner, t TrackEntry) error {
2727
if err != nil {
2828
return fmt.Errorf("matroska: could not create block struct: %w", err)
2929
}
30-
if block.TrackNumber() == t.TrackNumber {
31-
blocks = append(blocks, block)
30+
if block.TrackNumber() != t.TrackNumber {
31+
continue
3232
}
33+
blocks = append(blocks, block)
3334
}
3435
for i := range c.BlockGroup {
3536
block, err := ReadBlock(c.BlockGroup[i].Block, c.Timestamp)
3637
if err != nil {
3738
return fmt.Errorf("matroska: could not create block struct: %w", err)
3839
}
39-
if block.TrackNumber() == t.TrackNumber {
40-
blocks = append(blocks, block)
40+
if block.TrackNumber() != t.TrackNumber {
41+
continue
4142
}
43+
blocks = append(blocks, block)
4244
}
4345
}
4446
_ = scale

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ toolchain go1.24.1
77
require (
88
github.com/charmbracelet/huh v0.6.0
99
github.com/charmbracelet/huh/spinner v0.0.0-20250410174039-76d1f8226680
10-
github.com/coding-socks/ebml v0.0.0-20250409185428-3a0bcb87ec98
10+
github.com/coding-socks/ebml v0.0.0-20250413191753-908e87a1adf5
1111
github.com/spf13/pflag v1.0.6
1212
golang.org/x/tools v0.32.0
1313
)
@@ -41,3 +41,5 @@ require (
4141
golang.org/x/sys v0.32.0 // indirect
4242
golang.org/x/text v0.24.0 // indirect
4343
)
44+
45+
tool github.com/coding-socks/ebml

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ
3232
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
3333
github.com/coding-socks/ebml v0.0.0-20250409185428-3a0bcb87ec98 h1:OMkiM9+3+Q0Q+6r6s5Y3xJXNrFq6uAdfODbTF3iUxmo=
3434
github.com/coding-socks/ebml v0.0.0-20250409185428-3a0bcb87ec98/go.mod h1:6RUr58mWiQHHZiL2UFb8JygHNS5E35wGJfEmoSsDyI0=
35+
github.com/coding-socks/ebml v0.0.0-20250413191753-908e87a1adf5 h1:fgjgpV5PH7z03eKverW8/e7F2c/HlSVigr57N7HslWw=
36+
github.com/coding-socks/ebml v0.0.0-20250413191753-908e87a1adf5/go.mod h1:6RUr58mWiQHHZiL2UFb8JygHNS5E35wGJfEmoSsDyI0=
3537
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
3638
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
3739
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=

internal/ogg/crc32.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package ogg
2+
3+
import (
4+
"hash"
5+
"hash/crc32"
6+
)
7+
8+
var crcTable = makeOggCRC32Table()
9+
10+
func makeOggCRC32Table() *crc32.Table {
11+
crc32.NewIEEE()
12+
t := new(crc32.Table)
13+
for i := 0; i < 256; i++ {
14+
r := uint32(i) << 24
15+
for j := 0; j < 8; j++ {
16+
if r&0x80000000 != 0 {
17+
r = (r << 1) ^ 0x04c11db7
18+
} else {
19+
r <<= 1
20+
}
21+
}
22+
t[i] = r
23+
}
24+
return t
25+
}
26+
27+
func update(crc uint32, tab *crc32.Table, data []byte) uint32 {
28+
for _, v := range data {
29+
crc = (crc << 8) ^ tab[byte(crc>>24)^v]
30+
}
31+
return crc
32+
}
33+
34+
func CRC32Checksum(data []byte) uint32 {
35+
return update(0, crcTable, data)
36+
}
37+
38+
// digest represents the partial evaluation of a checksum.
39+
type digest struct {
40+
crc uint32
41+
tab *crc32.Table
42+
}
43+
44+
func NewCRC32() hash.Hash32 {
45+
return &digest{0, crcTable}
46+
}
47+
48+
func (d *digest) Size() int { return crc32.Size }
49+
50+
func (d *digest) BlockSize() int { return 1 }
51+
52+
func (d *digest) Reset() { d.crc = 0 }
53+
54+
func (d *digest) Write(p []byte) (n int, err error) {
55+
// We only create digest objects through New() which takes care of
56+
// initialization in this case.
57+
d.crc = update(d.crc, d.tab, p)
58+
return len(p), nil
59+
}
60+
61+
func (d *digest) Sum32() uint32 { return d.crc }
62+
63+
func (d *digest) Sum(in []byte) []byte {
64+
s := d.Sum32()
65+
return append(in, byte(s>>24), byte(s>>16), byte(s>>8), byte(s))
66+
}

internal/ogg/crc32_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package ogg
2+
3+
import (
4+
"io"
5+
"testing"
6+
)
7+
8+
func TestSum32(t *testing.T) {
9+
c := NewCRC32()
10+
in := "123456789"
11+
io.WriteString(c, in)
12+
s := c.Sum32()
13+
if out := uint32(0x89a1897f); s != out {
14+
t.Fatalf("jones crc64(%s) = 0x%x want 0x%x", in, s, out)
15+
}
16+
}

0 commit comments

Comments
 (0)