add podcast
This commit is contained in:
@@ -122,6 +122,32 @@ func ReadUint32(r io.Reader) (uint32, error) {
|
||||
return binary.LittleEndian.Uint32(b), nil
|
||||
}
|
||||
|
||||
// WriteUint16 writes a uint16 using a pooled buffer.
|
||||
func WriteUint16(w io.Writer, v uint16) error {
|
||||
bp := smallBufPool.Get().(*[]byte)
|
||||
defer smallBufPool.Put(bp)
|
||||
b := *bp
|
||||
|
||||
binary.LittleEndian.PutUint16(b, v)
|
||||
_, err := w.Write(b[:2])
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadUint16 reads a uint16 using a pooled buffer.
|
||||
func ReadUint16(r io.Reader) (uint16, error) {
|
||||
bp := smallBufPool.Get().(*[]byte)
|
||||
defer smallBufPool.Put(bp)
|
||||
b := (*bp)[:2]
|
||||
|
||||
// Read exactly 2 bytes into the slice.
|
||||
if _, err := io.ReadFull(r, b); err != nil {
|
||||
return 0, errors.Wrap(err, "read uint16")
|
||||
}
|
||||
|
||||
return binary.LittleEndian.Uint16(b), nil
|
||||
}
|
||||
|
||||
// WriteFloat32 writes a float32 using a pooled buffer.
|
||||
func WriteFloat32(w io.Writer, v float32) error {
|
||||
return WriteUint32(w, math.Float32bits(v))
|
||||
|
||||
100
pkg/util/wav/wav.go
Normal file
100
pkg/util/wav/wav.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright (C) 2025 wangyusong
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program 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 Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package wav
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
binaryutil "github.com/glidea/zenfeed/pkg/util/binary"
|
||||
)
|
||||
|
||||
// Header contains the WAV header information.
|
||||
type Header struct {
|
||||
SampleRate uint32
|
||||
BitDepth uint16
|
||||
NumChannels uint16
|
||||
}
|
||||
|
||||
// WriteHeader writes the WAV header to a writer.
|
||||
// pcmDataSize is the size of the raw PCM data.
|
||||
func WriteHeader(w io.Writer, h *Header, pcmDataSize uint32) error {
|
||||
// RIFF Header.
|
||||
if err := writeRIFFHeader(w, pcmDataSize); err != nil {
|
||||
return errors.Wrap(err, "write RIFF header")
|
||||
}
|
||||
|
||||
// fmt chunk.
|
||||
if err := writeFMTChunk(w, h); err != nil {
|
||||
return errors.Wrap(err, "write fmt chunk")
|
||||
}
|
||||
|
||||
// data chunk.
|
||||
if _, err := w.Write([]byte("data")); err != nil {
|
||||
return errors.Wrap(err, "write data chunk marker")
|
||||
}
|
||||
if err := binaryutil.WriteUint32(w, pcmDataSize); err != nil {
|
||||
return errors.Wrap(err, "write pcm data size")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeRIFFHeader(w io.Writer, pcmDataSize uint32) error {
|
||||
if _, err := w.Write([]byte("RIFF")); err != nil {
|
||||
return errors.Wrap(err, "write RIFF")
|
||||
}
|
||||
if err := binaryutil.WriteUint32(w, uint32(36+pcmDataSize)); err != nil {
|
||||
return errors.Wrap(err, "write file size")
|
||||
}
|
||||
if _, err := w.Write([]byte("WAVE")); err != nil {
|
||||
return errors.Wrap(err, "write WAVE")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeFMTChunk(w io.Writer, h *Header) error {
|
||||
if _, err := w.Write([]byte("fmt ")); err != nil {
|
||||
return errors.Wrap(err, "write fmt")
|
||||
}
|
||||
if err := binaryutil.WriteUint32(w, uint32(16)); err != nil { // PCM chunk size.
|
||||
return errors.Wrap(err, "write pcm chunk size")
|
||||
}
|
||||
if err := binaryutil.WriteUint16(w, uint16(1)); err != nil { // PCM format.
|
||||
return errors.Wrap(err, "write pcm format")
|
||||
}
|
||||
if err := binaryutil.WriteUint16(w, h.NumChannels); err != nil {
|
||||
return errors.Wrap(err, "write num channels")
|
||||
}
|
||||
if err := binaryutil.WriteUint32(w, h.SampleRate); err != nil {
|
||||
return errors.Wrap(err, "write sample rate")
|
||||
}
|
||||
byteRate := h.SampleRate * uint32(h.NumChannels) * uint32(h.BitDepth) / 8
|
||||
if err := binaryutil.WriteUint32(w, byteRate); err != nil {
|
||||
return errors.Wrap(err, "write byte rate")
|
||||
}
|
||||
blockAlign := h.NumChannels * h.BitDepth / 8
|
||||
if err := binaryutil.WriteUint16(w, blockAlign); err != nil {
|
||||
return errors.Wrap(err, "write block align")
|
||||
}
|
||||
if err := binaryutil.WriteUint16(w, h.BitDepth); err != nil {
|
||||
return errors.Wrap(err, "write bit depth")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
161
pkg/util/wav/wav_test.go
Normal file
161
pkg/util/wav/wav_test.go
Normal file
@@ -0,0 +1,161 @@
|
||||
// Copyright (C) 2025 wangyusong
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program 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 Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package wav
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/glidea/zenfeed/pkg/test"
|
||||
)
|
||||
|
||||
func TestWriteHeader(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
|
||||
type givenDetail struct{}
|
||||
|
||||
type whenDetail struct {
|
||||
header *Header
|
||||
pcmDataSize uint32
|
||||
}
|
||||
|
||||
type thenExpected struct {
|
||||
expectedBytes []byte
|
||||
expectError bool
|
||||
}
|
||||
|
||||
tests := []test.Case[givenDetail, whenDetail, thenExpected]{
|
||||
{
|
||||
Scenario: "Standard CD quality audio",
|
||||
Given: "a header for CD quality audio and a non-zero data size",
|
||||
When: "writing the header",
|
||||
Then: "should produce a valid 44-byte WAV header and no error",
|
||||
GivenDetail: givenDetail{},
|
||||
WhenDetail: whenDetail{
|
||||
header: &Header{
|
||||
SampleRate: 44100,
|
||||
BitDepth: 16,
|
||||
NumChannels: 2,
|
||||
},
|
||||
pcmDataSize: 176400,
|
||||
},
|
||||
ThenExpected: thenExpected{
|
||||
expectedBytes: []byte{
|
||||
'R', 'I', 'F', 'F',
|
||||
0x34, 0xB1, 0x02, 0x00, // ChunkSize = 36 + 176400 = 176436
|
||||
'W', 'A', 'V', 'E',
|
||||
'f', 'm', 't', ' ',
|
||||
0x10, 0x00, 0x00, 0x00, // Subchunk1Size = 16
|
||||
0x01, 0x00, // AudioFormat = 1 (PCM)
|
||||
0x02, 0x00, // NumChannels = 2
|
||||
0x44, 0xAC, 0x00, 0x00, // SampleRate = 44100
|
||||
0x10, 0xB1, 0x02, 0x00, // ByteRate = 176400
|
||||
0x04, 0x00, // BlockAlign = 4
|
||||
0x10, 0x00, // BitsPerSample = 16
|
||||
'd', 'a', 't', 'a',
|
||||
0x10, 0xB1, 0x02, 0x00, // Subchunk2Size = 176400
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
Scenario: "Mono audio for speech",
|
||||
Given: "a header for mono speech audio and a non-zero data size",
|
||||
When: "writing the header",
|
||||
Then: "should produce a valid 44-byte WAV header and no error",
|
||||
GivenDetail: givenDetail{},
|
||||
WhenDetail: whenDetail{
|
||||
header: &Header{
|
||||
SampleRate: 16000,
|
||||
BitDepth: 16,
|
||||
NumChannels: 1,
|
||||
},
|
||||
pcmDataSize: 32000,
|
||||
},
|
||||
ThenExpected: thenExpected{
|
||||
expectedBytes: []byte{
|
||||
'R', 'I', 'F', 'F',
|
||||
0x24, 0x7D, 0x00, 0x00, // ChunkSize = 36 + 32000 = 32036
|
||||
'W', 'A', 'V', 'E',
|
||||
'f', 'm', 't', ' ',
|
||||
0x10, 0x00, 0x00, 0x00, // Subchunk1Size = 16
|
||||
0x01, 0x00, // AudioFormat = 1
|
||||
0x01, 0x00, // NumChannels = 1
|
||||
0x80, 0x3E, 0x00, 0x00, // SampleRate = 16000
|
||||
0x00, 0x7D, 0x00, 0x00, // ByteRate = 32000
|
||||
0x02, 0x00, // BlockAlign = 2
|
||||
0x10, 0x00, // BitsPerSample = 16
|
||||
'd', 'a', 't', 'a',
|
||||
0x00, 0x7D, 0x00, 0x00, // Subchunk2Size = 32000
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
Scenario: "8-bit mono audio with zero data size",
|
||||
Given: "a header for 8-bit mono audio and a zero data size",
|
||||
When: "writing the header for an empty file",
|
||||
Then: "should produce a valid 44-byte WAV header with data size 0",
|
||||
GivenDetail: givenDetail{},
|
||||
WhenDetail: whenDetail{
|
||||
header: &Header{
|
||||
SampleRate: 8000,
|
||||
BitDepth: 8,
|
||||
NumChannels: 1,
|
||||
},
|
||||
pcmDataSize: 0,
|
||||
},
|
||||
ThenExpected: thenExpected{
|
||||
expectedBytes: []byte{
|
||||
'R', 'I', 'F', 'F',
|
||||
0x24, 0x00, 0x00, 0x00, // ChunkSize = 36 + 0 = 36
|
||||
'W', 'A', 'V', 'E',
|
||||
'f', 'm', 't', ' ',
|
||||
0x10, 0x00, 0x00, 0x00, // Subchunk1Size = 16
|
||||
0x01, 0x00, // AudioFormat = 1
|
||||
0x01, 0x00, // NumChannels = 1
|
||||
0x40, 0x1F, 0x00, 0x00, // SampleRate = 8000
|
||||
0x40, 0x1F, 0x00, 0x00, // ByteRate = 8000
|
||||
0x01, 0x00, // BlockAlign = 1
|
||||
0x08, 0x00, // BitsPerSample = 8
|
||||
'd', 'a', 't', 'a',
|
||||
0x00, 0x00, 0x00, 0x00, // Subchunk2Size = 0
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.Scenario, func(t *testing.T) {
|
||||
// Given.
|
||||
var buf bytes.Buffer
|
||||
|
||||
// When.
|
||||
err := WriteHeader(&buf, tt.WhenDetail.header, tt.WhenDetail.pcmDataSize)
|
||||
|
||||
// Then.
|
||||
if tt.ThenExpected.expectError {
|
||||
Expect(err).To(HaveOccurred())
|
||||
} else {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(buf.Bytes()).To(Equal(tt.ThenExpected.expectedBytes))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user