Skip to content

Commit

Permalink
decode cue chunk (encoder's missing)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattetti committed Jan 29, 2018
1 parent 840b3f0 commit d77e45d
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 50 deletions.
107 changes: 107 additions & 0 deletions cue_chunk.go
@@ -0,0 +1,107 @@
package wav

import (
"bytes"
"encoding/binary"
"fmt"

"github.com/mattetti/audio/riff"
)

// CuePoint defines an offset which marks a noteworthy sections of the audio
// content. For example, the beginning and end of a verse in a song may have cue
// points to make them easier to find.
type CuePoint struct {
// ID is the unique identifier of the cue point
ID [4]byte
// Play position specifies the sample offset associated with the cue point
// in terms of the sample's position in the final stream of samples
// generated by the play list. If a play list chunk is
// specified, the position value is equal to the sample number at which this
// cue point will occur during playback of the entire play list as defined
// by the play list's order. If no play list chunk is specified this value
// should be 0.
Position uint32
// DataChunkID - This value specifies the four byte ID used by the chunk
// containing the sample that corresponds to this cue point. A Wave file
// with no play list is always "data". A Wave file with a play list
// containing both sample data and silence may be either "data" or "slnt".
DataChunkID [4]byte
// ChunkStart specifies the byte offset into the Wave List Chunk of the
// chunk containing the sample that corresponds to this cue point. This is
// the same chunk described by the Data Chunk ID value. If no Wave List
// Chunk exists in the Wave file, this value is 0. If a Wave List Chunk
// exists, this is the offset into the "wavl" chunk. The first chunk in the
// Wave List Chunk would be specified with a value of 0.
ChunkStart uint32
// BlockStart specifies the byte offset into the "data" or "slnt" Chunk to
// the start of the block containing the sample. The start of a block is
// defined as the first byte in uncompressed PCM wave data or the last byte
// in compressed wave data where decompression can begin to find the value
// of the corresponding sample value.
BlockStart uint32
// SampleOffset specifies an offset into the block (specified by Block
// Start) for the sample that corresponds to the cue point. In uncompressed
// PCM waveform data, this is simply the byte offset into the "data" chunk.
// In compressed waveform data, this value is equal to the number of samples
// (may or may not be bytes) from the Block Start to the sample that
// corresponds to the cue point.
SampleOffset uint32
}

// DecodeCueChunk decodes the optional cue chunk and extracts cue points.
func DecodeCueChunk(d *Decoder, ch *riff.Chunk) error {
if ch == nil {
return fmt.Errorf("can't decode a nil chunk")
}
if d == nil {
return fmt.Errorf("nil decoder")
}
if ch.ID == CIDCue {
// read the entire chunk in memory
buf := make([]byte, ch.Size)
var err error
if _, err = ch.Read(buf); err != nil {
return fmt.Errorf("failed to read the CUE chunk - %v", err)
}
r := bytes.NewReader(buf)
var nbrCues uint32
if err := binary.Read(r, binary.LittleEndian, &nbrCues); err != nil {
return fmt.Errorf("failed to read the number of cues - %v", err)
}
if nbrCues > 0 {
fmt.Println("nbrCues", nbrCues)
if d.Metadata == nil {
d.Metadata = &Metadata{}
}
d.Metadata.CuePoints = []*CuePoint{}
scratch := make([]byte, 4)
for i := uint32(0); i < nbrCues; i++ {
c := &CuePoint{}
if _, err = r.Read(scratch); err != nil {
return fmt.Errorf("failed to read the cue point ID")
}
copy(c.ID[:], scratch[:4])
if err := binary.Read(r, binary.LittleEndian, &c.Position); err != nil {
return err
}
if _, err = r.Read(scratch); err != nil {
return fmt.Errorf("failed to read the data chunk id")
}
copy(c.DataChunkID[:], scratch[:4])
if err := binary.Read(r, binary.LittleEndian, &c.ChunkStart); err != nil {
return err
}
if err := binary.Read(r, binary.LittleEndian, &c.BlockStart); err != nil {
return err
}
if err := binary.Read(r, binary.LittleEndian, &c.SampleOffset); err != nil {
return err
}
d.Metadata.CuePoints = append(d.Metadata.CuePoints, c)
}
}

}
return nil
}
8 changes: 8 additions & 0 deletions decoder.go
Expand Up @@ -20,6 +20,8 @@ var (
CIDSmpl = [4]byte{'s', 'm', 'p', 'l'}
// CIDINFO is the chunk ID for an INFO chunk
CIDInfo = []byte{'I', 'N', 'F', 'O'}
// CIDCue is the chunk ID for the cue chunk
CIDCue = [4]byte{'c', 'u', 'e', 0x20}
)

// Decoder handles the decoding of wav files.
Expand Down Expand Up @@ -152,6 +154,12 @@ func (d *Decoder) ReadMetadata() {
d.err = err
}
}
case CIDCue:
if err = DecodeCueChunk(d, chunk); err != nil {
if err != io.EOF {
d.err = err
}
}
default:
// fmt.Println(string(chunk.ID[:]))
chunk.Drain()
Expand Down
49 changes: 0 additions & 49 deletions decoder_test.go
Expand Up @@ -3,7 +3,6 @@ package wav_test
import (
"os"
"path/filepath"
"reflect"
"testing"
"time"

Expand Down Expand Up @@ -153,54 +152,6 @@ func TestDecoder_Attributes(t *testing.T) {
}
}

func TestDecoder_ReadMetadata(t *testing.T) {
testCases := []struct {
in string
metadata *wav.Metadata
}{
{in: "fixtures/listinfo.wav",
metadata: &wav.Metadata{
Artist: "artist", Title: "track title", Product: "album title",
TrackNbr: "42", CreationDate: "2017", Genre: "genre", Comments: "my comment",
},
},
{in: "fixtures/kick.wav"},
{in: "fixtures/flloop.wav", metadata: &wav.Metadata{
Software: "FL Studio (beta)",
SamplerInfo: &wav.SamplerInfo{SamplePeriod: 22676, MIDIUnityNote: 60, NumSampleLoops: 1,
Loops: []*wav.SampleLoop{
{CuePointID: [4]byte{0, 0, 2, 0}, Type: 1024, Start: 0, End: 107999, Fraction: 0, PlayCount: 0},
}},
}},
}

for _, tc := range testCases {
t.Run(tc.in, func(t *testing.T) {
f, err := os.Open(tc.in)
if err != nil {
t.Fatal(err)
}
d := wav.NewDecoder(f)
d.ReadMetadata()
if err = d.Err(); err != nil {
t.Fatal(err)
}
if tc.metadata != nil {
if tc.metadata.SamplerInfo != nil {
if !reflect.DeepEqual(tc.metadata.SamplerInfo, d.Metadata.SamplerInfo) {
t.Fatalf("Expected sampler info\n%#v to equal\n%#v\n", d.Metadata.SamplerInfo, tc.metadata.SamplerInfo)
}
}

if !reflect.DeepEqual(tc.metadata, d.Metadata) {
t.Fatalf("Expected\n%#v\n to equal\n%#v\n", d.Metadata, tc.metadata)
}
}
f.Close()
})
}
}

func TestDecoderMisalignedInstChunk(t *testing.T) {
f, err := os.Open("fixtures/misaligned-chunk.wav")
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion examples_test.go
Expand Up @@ -99,5 +99,5 @@ func ExampleDecoder_ReadMetadata() {
}
fmt.Printf("%#v\n", d.Metadata)
// Output:
// &wav.Metadata{SamplerInfo:(*wav.SamplerInfo)(nil), Artist:"artist", Comments:"my comment", Copyright:"", CreationDate:"2017", Engineer:"", Technician:"", Genre:"genre", Keywords:"", Medium:"", Title:"track title", Product:"album title", Subject:"", Software:"", Source:"", Location:"", TrackNbr:"42"}
// &wav.Metadata{SamplerInfo:(*wav.SamplerInfo)(nil), Artist:"artist", Comments:"my comment", Copyright:"", CreationDate:"2017", Engineer:"", Technician:"", Genre:"genre", Keywords:"", Medium:"", Title:"track title", Product:"album title", Subject:"", Software:"", Source:"", Location:"", TrackNbr:"42", CuePoints:[]*wav.CuePoint(nil)}
}
2 changes: 2 additions & 0 deletions metadata.go
Expand Up @@ -48,6 +48,8 @@ type Metadata struct {
Location string
// TrackNbr is the track number
TrackNbr string
// CuePoints is a list of cue points in the wav file.
CuePoints []*CuePoint
}

// SamplerInfo is extra metadata pertinent to a sampler type usage.
Expand Down
85 changes: 85 additions & 0 deletions metadata_test.go
@@ -0,0 +1,85 @@
package wav_test

import (
"os"
"reflect"
"testing"

"github.com/go-audio/wav"
)

func TestDecoder_ReadMetadata(t *testing.T) {
testCases := []struct {
in string
metadata *wav.Metadata
}{
{in: "fixtures/listinfo.wav",
metadata: &wav.Metadata{
Artist: "artist", Title: "track title", Product: "album title",
TrackNbr: "42", CreationDate: "2017", Genre: "genre", Comments: "my comment",
},
},
{in: "fixtures/kick.wav"},
{in: "fixtures/flloop.wav", metadata: &wav.Metadata{
Software: "FL Studio (beta)",
CuePoints: []*wav.CuePoint{
0: {ID: [4]uint8{0x1, 0x0, 0x0, 0x0}, Position: 0x0, DataChunkID: [4]uint8{'d', 'a', 't', 'a'}},
1: {ID: [4]uint8{0x2, 0x0, 0x0, 0x0}, Position: 0x1a5e, DataChunkID: [4]uint8{'d', 'a', 't', 'a'}, SampleOffset: 0x1a5e},
2: {ID: [4]uint8{0x3, 0x0, 0x0, 0x0}, Position: 0x34bc, DataChunkID: [4]uint8{'d', 'a', 't', 'a'}, SampleOffset: 0x34bc},
3: {ID: [4]uint8{0x4, 0x0, 0x0, 0x0}, Position: 0x4f1a, DataChunkID: [4]uint8{'d', 'a', 't', 'a'}, SampleOffset: 0x4f1a},
4: {ID: [4]uint8{0x5, 0x0, 0x0, 0x0}, Position: 0x6978, DataChunkID: [4]uint8{'d', 'a', 't', 'a'}, SampleOffset: 0x6978},
5: {ID: [4]uint8{0x6, 0x0, 0x0, 0x0}, Position: 0x83d6, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0x83d6},
6: {ID: [4]uint8{0x7, 0x0, 0x0, 0x0}, Position: 0x9e34, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0x9e34},
7: {ID: [4]uint8{0x8, 0x0, 0x0, 0x0}, Position: 0xb892, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0xb892},
8: {ID: [4]uint8{0x9, 0x0, 0x0, 0x0}, Position: 0xd2f0, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0xd2f0},
9: {ID: [4]uint8{0xa, 0x0, 0x0, 0x0}, Position: 0xed4e, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0xed4e},
10: {ID: [4]uint8{0xb, 0x0, 0x0, 0x0}, Position: 0x107ac, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0x107ac},
11: {ID: [4]uint8{0xc, 0x0, 0x0, 0x0}, Position: 0x1220a, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0x1220a},
12: {ID: [4]uint8{0xd, 0x0, 0x0, 0x0}, Position: 0x13c68, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0x13c68},
13: {ID: [4]uint8{0xe, 0x0, 0x0, 0x0}, Position: 0x156c6, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0x156c6},
14: {ID: [4]uint8{0xf, 0x0, 0x0, 0x0}, Position: 0x17124, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0x17124},
15: {ID: [4]uint8{0x10, 0x0, 0x0, 0x0}, Position: 0x18b82, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0x18b82},
},
SamplerInfo: &wav.SamplerInfo{SamplePeriod: 22676, MIDIUnityNote: 60, NumSampleLoops: 1,
Loops: []*wav.SampleLoop{
{CuePointID: [4]byte{0, 0, 2, 0}, Type: 1024, Start: 0, End: 107999, Fraction: 0, PlayCount: 0},
}},
}},
}

for _, tc := range testCases {
t.Run(tc.in, func(t *testing.T) {
f, err := os.Open(tc.in)
if err != nil {
t.Fatal(err)
}
d := wav.NewDecoder(f)
d.ReadMetadata()
if err = d.Err(); err != nil {
t.Fatal(err)
}
if tc.metadata != nil {
if tc.metadata.SamplerInfo != nil {
if !reflect.DeepEqual(tc.metadata.SamplerInfo, d.Metadata.SamplerInfo) {
t.Fatalf("Expected sampler info\n%#v to equal\n%#v\n", d.Metadata.SamplerInfo, tc.metadata.SamplerInfo)
}
}
if tc.metadata.CuePoints != nil {
if !reflect.DeepEqual(tc.metadata.CuePoints, d.Metadata.CuePoints) {
for i, c := range d.Metadata.CuePoints {
if !reflect.DeepEqual(c, tc.metadata.CuePoints[i]) {
t.Errorf("[%d] expected %#v got %#v", i, tc.metadata.CuePoints[i], c)
}
}
t.Errorf("Expected cue points\n%#v to equal\n%#v\n", d.Metadata.CuePoints, tc.metadata.CuePoints)
}
}

if !reflect.DeepEqual(tc.metadata, d.Metadata) {
t.Fatalf("Expected\n%#v\n to equal\n%#v\n", d.Metadata, tc.metadata)
}
}
f.Close()
})
}
}

0 comments on commit d77e45d

Please sign in to comment.