/
cuesheet.go
242 lines (222 loc) · 6.32 KB
/
cuesheet.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
package meta
import (
"encoding/binary"
"errors"
"fmt"
"io"
"io/ioutil"
"strings"
)
// A CueSheet describes how tracks are laid out within a FLAC stream.
//
// ref: https://www.xiph.org/flac/format.html#metadata_block_cuesheet
type CueSheet struct {
// Media catalog number.
MCN string
// Number of lead-in samples. This field only has meaning for CD-DA cue
// sheets; for other uses it should be 0. Refer to the spec for additional
// information.
NLeadInSamples uint64
// Specifies if the cue sheet corresponds to a Compact Disc.
IsCompactDisc bool
// One or more tracks. The last track of a cue sheet is always the lead-out
// track.
Tracks []CueSheetTrack
}
// parseCueSheet reads and parses the body of a CueSheet metadata block.
func (block *Block) parseCueSheet() error {
// Parse cue sheet.
// 128 bytes: MCN.
szMCN, err := readString(block.lr, 128)
if err != nil {
return unexpected(err)
}
cs := &CueSheet{
MCN: stringFromSZ(szMCN),
}
block.Body = cs
// 64 bits: NLeadInSamples.
if err = binary.Read(block.lr, binary.BigEndian, &cs.NLeadInSamples); err != nil {
return unexpected(err)
}
// 1 bit: IsCompactDisc.
var x uint8
if err := binary.Read(block.lr, binary.BigEndian, &x); err != nil {
return unexpected(err)
}
// mask = 10000000
if x&0x80 != 0 {
cs.IsCompactDisc = true
}
// 7 bits and 258 bytes: reserved.
// mask = 01111111
if x&0x7F != 0 {
return ErrInvalidPadding
}
lr := io.LimitReader(block.lr, 258)
zr := zeros{r: lr}
if _, err := io.Copy(ioutil.Discard, zr); err != nil {
return err
}
// Parse cue sheet tracks.
// 8 bits: (number of tracks)
if err := binary.Read(block.lr, binary.BigEndian, &x); err != nil {
return unexpected(err)
}
if x < 1 {
return errors.New("meta.Block.parseCueSheet: at least one track required")
}
if cs.IsCompactDisc && x > 100 {
return fmt.Errorf("meta.Block.parseCueSheet: number of CD-DA tracks (%d) exceeds 100", x)
}
cs.Tracks = make([]CueSheetTrack, x)
// Each track number within a cue sheet must be unique; use uniq to keep
// track.
uniq := make(map[uint8]struct{})
for i := range cs.Tracks {
if err := block.parseTrack(cs, i, uniq); err != nil {
return err
}
}
return nil
}
// parseTrack parses the i:th cue sheet track, and ensures that its track number
// is unique.
func (block *Block) parseTrack(cs *CueSheet, i int, uniq map[uint8]struct{}) error {
track := &cs.Tracks[i]
// 64 bits: Offset.
if err := binary.Read(block.lr, binary.BigEndian, &track.Offset); err != nil {
return unexpected(err)
}
if cs.IsCompactDisc && track.Offset%588 != 0 {
return fmt.Errorf("meta.Block.parseCueSheet: CD-DA track offset (%d) must be evenly divisible by 588", track.Offset)
}
// 8 bits: Num.
if err := binary.Read(block.lr, binary.BigEndian, &track.Num); err != nil {
return unexpected(err)
}
if _, ok := uniq[track.Num]; ok {
return fmt.Errorf("meta.Block.parseCueSheet: duplicated track number %d", track.Num)
}
uniq[track.Num] = struct{}{}
if track.Num == 0 {
return errors.New("meta.Block.parseCueSheet: invalid track number (0)")
}
isLeadOut := i == len(cs.Tracks)-1
if cs.IsCompactDisc {
if !isLeadOut {
if track.Num >= 100 {
return fmt.Errorf("meta.Block.parseCueSheet: CD-DA track number (%d) exceeds 99", track.Num)
}
} else {
if track.Num != 170 {
return fmt.Errorf("meta.Block.parseCueSheet: invalid lead-out CD-DA track number; expected 170, got %d", track.Num)
}
}
} else {
if isLeadOut && track.Num != 255 {
return fmt.Errorf("meta.Block.parseCueSheet: invalid lead-out track number; expected 255, got %d", track.Num)
}
}
// 12 bytes: ISRC.
szISRC, err := readString(block.lr, 12)
if err != nil {
return unexpected(err)
}
track.ISRC = stringFromSZ(szISRC)
// 1 bit: IsAudio.
var x uint8
if err = binary.Read(block.lr, binary.BigEndian, &x); err != nil {
return unexpected(err)
}
// mask = 10000000
if x&0x80 == 0 {
track.IsAudio = true
}
// 1 bit: HasPreEmphasis.
// mask = 01000000
if x&0x40 != 0 {
track.HasPreEmphasis = true
}
// 6 bits and 13 bytes: reserved.
// mask = 00111111
if x&0x3F != 0 {
return ErrInvalidPadding
}
lr := io.LimitReader(block.lr, 13)
zr := zeros{r: lr}
_, err = io.Copy(ioutil.Discard, zr)
if err != nil {
return err
}
// Parse indicies.
// 8 bits: (number of indicies)
if err = binary.Read(block.lr, binary.BigEndian, &x); err != nil {
return unexpected(err)
}
if x < 1 {
if !isLeadOut {
return errors.New("meta.Block.parseCueSheet: at least one track index required")
}
// Lead-out track has no track indices to parse; return early.
return nil
}
track.Indicies = make([]CueSheetTrackIndex, x)
for i := range track.Indicies {
index := &track.Indicies[i]
// 64 bits: Offset.
if err = binary.Read(block.lr, binary.BigEndian, &index.Offset); err != nil {
return unexpected(err)
}
// 8 bits: Num.
if err = binary.Read(block.lr, binary.BigEndian, &index.Num); err != nil {
return unexpected(err)
}
// 3 bytes: reserved.
lr = io.LimitReader(block.lr, 3)
zr = zeros{r: lr}
_, err = io.Copy(ioutil.Discard, zr)
if err != nil {
return err
}
}
return nil
}
// stringFromSZ returns a copy of the given string terminated at the first
// occurrence of a NULL character.
func stringFromSZ(szStr string) string {
pos := strings.IndexByte(szStr, '\x00')
if pos == -1 {
return szStr
}
return string(szStr[:pos])
}
// CueSheetTrack contains the start offset of a track and other track specific
// metadata.
type CueSheetTrack struct {
// Track offset in samples, relative to the beginning of the FLAC audio
// stream.
Offset uint64
// Track number; never 0, always unique.
Num uint8
// International Standard Recording Code; empty string if not present.
//
// ref: http://isrc.ifpi.org/
ISRC string
// Specifies if the track contains audio or data.
IsAudio bool
// Specifies if the track has been recorded with pre-emphasis
HasPreEmphasis bool
// Every track has one or more track index points, except for the lead-out
// track which has zero. Each index point specifies a position within the
// track.
Indicies []CueSheetTrackIndex
}
// A CueSheetTrackIndex specifies a position within a track.
type CueSheetTrackIndex struct {
// Index point offset in samples, relative to the track offset.
Offset uint64
// Index point number; subsequently incrementing by 1 and always unique
// within a track.
Num uint8
}