add validated audio interleaving
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
package output
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"mxl-player/internal/playback"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidAudioFrame = errors.New("invalid audio frame")
|
||||||
|
ErrAudioPayloadTooSmall = errors.New("audio channel payload is too small")
|
||||||
|
)
|
||||||
|
|
||||||
|
func InterleaveF32(frame playback.AudioFrame) ([]byte, error) {
|
||||||
|
if frame.Channels == 0 ||
|
||||||
|
frame.SampleCount == 0 ||
|
||||||
|
frame.SampleRateNumerator <= 0 ||
|
||||||
|
frame.SampleRateDenominator <= 0 {
|
||||||
|
return nil, ErrInvalidAudioFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
if uint64(len(frame.Samples)) != frame.Channels {
|
||||||
|
return nil, ErrInvalidAudioFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame.SampleCount > ^uint64(0)/4 {
|
||||||
|
return nil, ErrInvalidAudioFrame
|
||||||
|
}
|
||||||
|
bytesPerChannel := frame.SampleCount * 4
|
||||||
|
|
||||||
|
if frame.Channels > ^uint64(0)/bytesPerChannel {
|
||||||
|
return nil, ErrInvalidAudioFrame
|
||||||
|
}
|
||||||
|
totalBytes := frame.Channels * bytesPerChannel
|
||||||
|
|
||||||
|
maxInt := uint64(^uint(0) >> 1)
|
||||||
|
if totalBytes > maxInt {
|
||||||
|
return nil, ErrInvalidAudioFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, samples := range frame.Samples {
|
||||||
|
if bytesPerChannel > uint64(len(samples)) {
|
||||||
|
return nil, ErrAudioPayloadTooSmall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]byte, int(totalBytes))
|
||||||
|
|
||||||
|
for sample := uint64(0); sample < frame.SampleCount; sample++ {
|
||||||
|
for channel := uint64(0); channel < frame.Channels; channel++ {
|
||||||
|
sourceOffset := sample * 4
|
||||||
|
destinationOffset := (sample*frame.Channels + channel) * 4
|
||||||
|
|
||||||
|
sourceStart := int(sourceOffset)
|
||||||
|
destinationStart := int(destinationOffset)
|
||||||
|
|
||||||
|
copy(
|
||||||
|
result[destinationStart:destinationStart+4],
|
||||||
|
frame.Samples[channel][sourceStart:sourceStart+4],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package output
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"mxl-player/internal/playback"
|
||||||
|
)
|
||||||
|
|
||||||
|
func validAudioFrame() playback.AudioFrame {
|
||||||
|
return playback.AudioFrame{
|
||||||
|
Index: 42,
|
||||||
|
SampleCount: 2,
|
||||||
|
Channels: 2,
|
||||||
|
SampleRateNumerator: 48000,
|
||||||
|
SampleRateDenominator: 1,
|
||||||
|
Samples: [][]byte{
|
||||||
|
{1, 2, 3, 4, 5, 6, 7, 8},
|
||||||
|
{9, 10, 11, 12, 13, 14, 15, 16},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterleaveF32OrdersSamplesByFrameThenChannel(t *testing.T) {
|
||||||
|
frame := validAudioFrame()
|
||||||
|
want := []byte{
|
||||||
|
1, 2, 3, 4,
|
||||||
|
9, 10, 11, 12,
|
||||||
|
5, 6, 7, 8,
|
||||||
|
13, 14, 15, 16,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := InterleaveF32(frame)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("InterleaveF32() error = %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Fatalf("InterleaveF32() = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterleaveF32IgnoresBytesAfterSampleCount(t *testing.T) {
|
||||||
|
frame := validAudioFrame()
|
||||||
|
frame.SampleCount = 1
|
||||||
|
|
||||||
|
got, err := InterleaveF32(frame)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("InterleaveF32() error = %v", err)
|
||||||
|
}
|
||||||
|
want := []byte{1, 2, 3, 4, 9, 10, 11, 12}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Fatalf("InterleaveF32() = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterleaveF32RejectsInvalidMetadata(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mutate func(*playback.AudioFrame)
|
||||||
|
}{
|
||||||
|
{name: "zero samples", mutate: func(f *playback.AudioFrame) { f.SampleCount = 0 }},
|
||||||
|
{name: "zero channels", mutate: func(f *playback.AudioFrame) { f.Channels = 0 }},
|
||||||
|
{name: "zero rate numerator", mutate: func(f *playback.AudioFrame) { f.SampleRateNumerator = 0 }},
|
||||||
|
{name: "negative rate numerator", mutate: func(f *playback.AudioFrame) { f.SampleRateNumerator = -1 }},
|
||||||
|
{name: "zero rate denominator", mutate: func(f *playback.AudioFrame) { f.SampleRateDenominator = 0 }},
|
||||||
|
{name: "wrong channel count", mutate: func(f *playback.AudioFrame) { f.Channels = 3 }},
|
||||||
|
{name: "sample size overflow", mutate: func(f *playback.AudioFrame) { f.SampleCount = ^uint64(0) }},
|
||||||
|
{
|
||||||
|
name: "allocation size overflow",
|
||||||
|
mutate: func(f *playback.AudioFrame) {
|
||||||
|
f.SampleCount = uint64(^uint(0)>>1) / 4
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
frame := validAudioFrame()
|
||||||
|
tt.mutate(&frame)
|
||||||
|
result, err := InterleaveF32(frame)
|
||||||
|
if result != nil {
|
||||||
|
t.Fatalf("InterleaveF32() result = %v, want nil", result)
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrInvalidAudioFrame) {
|
||||||
|
t.Fatalf("InterleaveF32() error = %v, want %v", err, ErrInvalidAudioFrame)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterleaveF32RejectsShortChannel(t *testing.T) {
|
||||||
|
frame := validAudioFrame()
|
||||||
|
frame.Samples[1] = frame.Samples[1][:7]
|
||||||
|
|
||||||
|
result, err := InterleaveF32(frame)
|
||||||
|
if result != nil {
|
||||||
|
t.Fatalf("InterleaveF32() result = %v, want nil", result)
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrAudioPayloadTooSmall) {
|
||||||
|
t.Fatalf("InterleaveF32() error = %v, want %v", err, ErrAudioPayloadTooSmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterleaveF32ReturnsIndependentOutput(t *testing.T) {
|
||||||
|
frame := validAudioFrame()
|
||||||
|
result, err := InterleaveF32(frame)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("InterleaveF32() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.Samples[0][0] = 99
|
||||||
|
if result[0] != 1 {
|
||||||
|
t.Fatalf("output changed with borrowed input: first byte = %d, want 1", result[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user