mirror of https://bitbucket.org/ausocean/av.git
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:
parent
19b696683b
commit
578e60823b
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue