diff --git a/cue_chunk.go b/cue_chunk.go new file mode 100644 index 0000000..a34c332 --- /dev/null +++ b/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 +} diff --git a/decoder.go b/decoder.go index aa0c9b0..dfe4e75 100644 --- a/decoder.go +++ b/decoder.go @@ -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. @@ -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() diff --git a/decoder_test.go b/decoder_test.go index 0860cea..eb8685f 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -3,7 +3,6 @@ package wav_test import ( "os" "path/filepath" - "reflect" "testing" "time" @@ -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 { diff --git a/examples_test.go b/examples_test.go index dd0bb6a..5adb374 100644 --- a/examples_test.go +++ b/examples_test.go @@ -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)} } diff --git a/metadata.go b/metadata.go index e3aab99..7bce523 100644 --- a/metadata.go +++ b/metadata.go @@ -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. diff --git a/metadata_test.go b/metadata_test.go new file mode 100644 index 0000000..d07f94f --- /dev/null +++ b/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() + }) + } +}