diff --git a/codec/wav/wav.go b/codec/wav/wav.go new file mode 100644 index 00000000..a4aef63d --- /dev/null +++ b/codec/wav/wav.go @@ -0,0 +1,131 @@ +/* +NAME + wav.go + +DESCRIPTION + wav.go contains functions for processing wav. + +AUTHOR + David Sutton + +LICENSE + wav.go is Copyright (C) 2023 the Australian Ocean Lab (AusOcean) + + It is free software: you can redistribute it and/or modify them + under the terms of the GNU General Public License as published by the + Free Software Foundation, either version 3 of the License, or (at your + option) any later version. + + It is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License in gpl.txt. + If not, see [GNU licenses](http://www.gnu.org/licenses). +*/ + +// Package wav provides functions for converting wav audio. +package wav + +import ( + "encoding/binary" + "fmt" +) + +const PCMFormat = 1 // PCMFormat defines the value for pcm audio as defined by the wav std. + +var ( + errInvalidFormat = fmt.Errorf("invalid or no format defined") + errInvalidRate = fmt.Errorf("invalid or no sample rate defined") + errInvalidChannels = fmt.Errorf("invalid or no number of channels defined") + errInvalidBitDepth = fmt.Errorf("invalid or no bit depth defined") +) + +// Metadata defines the format of the audio file for reading. +type Metadata struct { + AudioFormat int + Channels int + SampleRate int + BitDepth int +} + +type WAV struct { + Metadata Metadata + Audio []byte +} + +// Write writes the given audio byte slice to the WAV, encoding the appropriate headings. +func (w *WAV) Write(p []byte) (n int, err error) { + // Create header slice. + header := make([]byte, 44) + + // Write RIFF type. + copy(header[0:4], []byte("RIFF")) + + // Write the size of overall file. + buf := make([]byte, 4) + binary.LittleEndian.PutUint32(buf, uint32(len(p)+44)) + copy(header[4:8], buf) + + // Write WAVE type. + copy(header[8:12], []byte("WAVE")) + + // Write fmt chunk marker. + copy(header[12:16], []byte("fmt ")) + + // Write the subchunk1 Size. + binary.LittleEndian.PutUint32(buf, 16) + copy(header[16:20], buf) + + // Write the encoded audio format. + if w.Metadata.AudioFormat != PCMFormat { // TODO: allow for more encoding formats. + return 0, errInvalidFormat + } + binary.LittleEndian.PutUint16(buf[0:2], 1) + copy(header[20:22], buf[0:2]) + + // Write the number of channels. + if w.Metadata.Channels == 0 { + return 0, errInvalidChannels + } + binary.LittleEndian.PutUint16(buf[0:2], uint16(w.Metadata.Channels)) + copy(header[22:24], buf[0:2]) + + // Write the sample rate. + if w.Metadata.SampleRate == 0 { + return 0, errInvalidRate + } + binary.LittleEndian.PutUint32(buf[0:4], uint32(w.Metadata.SampleRate)) + copy(header[24:28], buf[0:4]) + + // Write bit depth values. + if w.Metadata.BitDepth == 0 { + return 0, errInvalidBitDepth + } + var val uint32 = uint32((w.Metadata.SampleRate * w.Metadata.BitDepth * w.Metadata.Channels) / 8) + binary.LittleEndian.PutUint32(buf[0:4], val) + copy(header[28:32], buf[0:4]) + + val = uint32((w.Metadata.BitDepth * w.Metadata.Channels) / 8) + binary.LittleEndian.PutUint32(buf[0:4], val) + copy(header[32:34], buf[0:4]) + + binary.LittleEndian.PutUint32(buf[0:4], uint32(w.Metadata.BitDepth)) + copy(header[34:36], buf[0:4]) + + // Mark start of data. + copy(header[36:40], []byte("data")) + + // Write size of data chunk. + binary.LittleEndian.PutUint32(buf[0:4], uint32(len(p))) + copy(header[40:44], buf[0:4]) + + // Append audio data. + w.Audio = header + w.Audio = append(w.Audio, p...) + + // Return successful write. + return len(p) + 44, nil + +} diff --git a/codec/wav/wav_test.go b/codec/wav/wav_test.go new file mode 100644 index 00000000..b0fdb1b6 --- /dev/null +++ b/codec/wav/wav_test.go @@ -0,0 +1,73 @@ +/* +NAME + wav.go + +DESCRIPTION + wav.go contains functions for processing wav. + +AUTHOR + David Sutton + +LICENSE + wav.go is Copyright (C) 2019 the Australian Ocean Lab (AusOcean) + + It is free software: you can redistribute it and/or modify them + under the terms of the GNU General Public License as published by the + Free Software Foundation, either version 3 of the License, or (at your + option) any later version. + + It is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License in gpl.txt. + If not, see [GNU licenses](http://www.gnu.org/licenses). +*/ + +// Package wav provides functions for converting wav audio. +package wav + +import ( + "testing" +) + +func TestWavWriter(t *testing.T) { + tests := []struct { + name string + md Metadata + input []byte + wantN int + wantErr error + }{ + {name: "Header Only", md: Metadata{AudioFormat: PCMFormat, Channels: 1, SampleRate: 48000, BitDepth: 16}, input: nil, wantN: 44, wantErr: nil}, + {name: "4 bytes", md: Metadata{AudioFormat: PCMFormat, Channels: 1, SampleRate: 48000, BitDepth: 16}, input: []byte{0,0,0,0}, wantN: 48, wantErr: nil}, + {name: "No format", md: Metadata{Channels: 1, SampleRate: 48000, BitDepth: 16}, input: []byte{0,0,0,0}, wantN: 0, wantErr: errInvalidFormat}, + {name: "Invalid format", md: Metadata{AudioFormat: 2, Channels: 1, SampleRate: 48000, BitDepth: 16}, input: []byte{0,0,0,0}, wantN: 0, wantErr: errInvalidFormat}, + {name: "No channels", md: Metadata{AudioFormat: PCMFormat, SampleRate: 48000, BitDepth: 16}, input: []byte{0,0,0,0}, wantN: 0, wantErr: errInvalidChannels}, + {name: "Invalid channels", md: Metadata{AudioFormat: PCMFormat, Channels: 0, SampleRate: 48000, BitDepth: 16}, input: []byte{0,0,0,0}, wantN: 0, wantErr: errInvalidChannels}, + {name: "No sample rate", md: Metadata{AudioFormat: PCMFormat, Channels: 1, BitDepth: 16}, input: []byte{0,0,0,0}, wantN: 0, wantErr: errInvalidRate}, + {name: "Invalid sample rate", md: Metadata{AudioFormat: PCMFormat, Channels: 1, SampleRate: 0, BitDepth: 16}, input: []byte{0,0,0,0}, wantN: 0, wantErr: errInvalidRate}, + {name: "No bit depth", md: Metadata{AudioFormat: PCMFormat, Channels: 1, SampleRate: 48000}, input: []byte{0,0,0,0}, wantN: 0, wantErr: errInvalidBitDepth}, + {name: "Invalid bit depth", md: Metadata{AudioFormat: PCMFormat, Channels: 1, SampleRate: 48000, BitDepth: 0}, input: []byte{0,0,0,0}, wantN: 0, wantErr: errInvalidBitDepth}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &WAV{ + Metadata: tt.md, + } + + gotN, err := w.Write(tt.input) + if err != tt.wantErr { + t.Errorf("WAV.Write() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if gotN != tt.wantN { + t.Errorf("WAV.Write() = %v, want %v", gotN, tt.wantN) + } + + }) + } +}