codec/wav: Implement a wav encoder

A wav encoder will be useful for returning easily playable audio files to users of vidgrind that are not familiar with PCM audio. Currently the encoder only support PCM data, but can be updated with other types. The encoder implements the writer interface.

* codec/wav: Add unit tests for wav encoder

Merged in create-wav-encoder (pull request #529)
Approved-by: Trek Hopton
This commit is contained in:
David Sutton 2023-12-04 00:36:29 +00:00
parent 19b696683b
commit 578e60823b
2 changed files with 204 additions and 0 deletions

131
codec/wav/wav.go Normal file
View File

@ -0,0 +1,131 @@
/*
NAME
wav.go
DESCRIPTION
wav.go contains functions for processing wav.
AUTHOR
David Sutton <davidsutton@ausocean.org>
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
}

73
codec/wav/wav_test.go Normal file
View File

@ -0,0 +1,73 @@
/*
NAME
wav.go
DESCRIPTION
wav.go contains functions for processing wav.
AUTHOR
David Sutton <davidsutton@ausocean.org>
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)
}
})
}
}