fix: tighten PipeWire buffer helper invariants
- commit
- 66c3355792f6d0576c462837103a6f42f41da9f0
- author
- brice · · 2026-04-10 13:27 UTC
- parent
- 177250e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +119 -68
M internal/core/pwbuffer.go +9 -6
| @@ -2,6 +2,7 @@ package core | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| 5 | + | "fmt" | |
| 5 | 6 | "unsafe" | |
| 6 | 7 | ) | |
| 7 | 8 | ||
| @@ -58,6 +59,7 @@ var ( | |||
| 58 | 59 | ErrNilBufferPointer = errors.New("nil buffer pointer") | |
| 59 | 60 | ErrChannelMismatch = errors.New("buffer data count does not match channels") | |
| 60 | 61 | ErrUnmappedData = errors.New("buffer data pointer is nil") | |
| 62 | + | ErrFrameOverflow = errors.New("requested frames exceed buffer capacity") | |
| 61 | 63 | ) | |
| 62 | 64 | ||
| 63 | 65 | // newPWBufferView interprets bufPtr as a *pwBuffer and returns a view that | |
| @@ -88,13 +90,12 @@ func newPWBufferView(bufPtr unsafe.Pointer, channels, frames int) (*pwBufferView | |||
| 88 | 90 | if data.Data == nil { | |
| 89 | 91 | return nil, ErrUnmappedData | |
| 90 | 92 | } | |
| 91 | - | // Slice the raw memory as []float32 for this channel. | |
| 92 | - | // Data.Maxsize gives the total buffer size in bytes for this channel. | |
| 93 | - | nFloats := frames | |
| 94 | - | if maxSize := int(data.Maxsize) / 4; nFloats > maxSize { | |
| 95 | - | nFloats = maxSize | |
| 93 | + | // Reject if the buffer cannot hold the requested number of frames. | |
| 94 | + | maxFrames := int(data.Maxsize) / 4 | |
| 95 | + | if frames > maxFrames { | |
| 96 | + | return nil, fmt.Errorf("%w: requested %d frames but buffer holds %d", ErrFrameOverflow, frames, maxFrames) | |
| 96 | 97 | } | |
| 97 | - | samples[i] = unsafe.Slice((*float32)(data.Data), nFloats) | |
| 98 | + | samples[i] = unsafe.Slice((*float32)(data.Data), frames) | |
| 98 | 99 | } | |
| 99 | 100 | ||
| 100 | 101 | return &pwBufferView{ | |
| @@ -107,10 +108,12 @@ func newPWBufferView(bufPtr unsafe.Pointer, channels, frames int) (*pwBufferView | |||
| 107 | 108 | ||
| 108 | 109 | // PCM returns a PCMBuffer pointing at the buffer's planar float32 data. | |
| 109 | 110 | // The caller can write into Samples and then call Commit. | |
| 111 | + | // Stride is set to 4 (bytes per float32 sample) for planar float32 layout. | |
| 110 | 112 | func (v *pwBufferView) PCM() *PCMBuffer { | |
| 111 | 113 | return &PCMBuffer{ | |
| 112 | 114 | Frames: v.frames, | |
| 113 | 115 | Channels: v.channels, | |
| 116 | + | Stride: 4, // sizeof(float32) — planar float32 byte stride per channel | |
| 114 | 117 | Samples: v.samples, | |
| 115 | 118 | } | |
| 116 | 119 | } | |
| @@ -2,6 +2,7 @@ package core | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "errors" | 4 | "errors" | ||
| 5 | + | "fmt" | |||
| 5 | "unsafe" | 6 | "unsafe" | ||
| 6 | ) | 7 | ) | ||
| 7 | 8 | ||||
| @@ -58,6 +59,7 @@ var ( | |||||
| 58 | ErrNilBufferPointer = errors.New("nil buffer pointer") | 59 | ErrNilBufferPointer = errors.New("nil buffer pointer") | ||
| 59 | ErrChannelMismatch = errors.New("buffer data count does not match channels") | 60 | ErrChannelMismatch = errors.New("buffer data count does not match channels") | ||
| 60 | ErrUnmappedData = errors.New("buffer data pointer is nil") | 61 | ErrUnmappedData = errors.New("buffer data pointer is nil") | ||
| 62 | + | ErrFrameOverflow = errors.New("requested frames exceed buffer capacity") | |||
| 61 | ) | 63 | ) | ||
| 62 | 64 | ||||
| 63 | // newPWBufferView interprets bufPtr as a *pwBuffer and returns a view that | 65 | // newPWBufferView interprets bufPtr as a *pwBuffer and returns a view that | ||
| @@ -88,13 +90,12 @@ func newPWBufferView(bufPtr unsafe.Pointer, channels, frames int) (*pwBufferView | |||||
| 88 | if data.Data == nil { | 90 | if data.Data == nil { | ||
| 89 | return nil, ErrUnmappedData | 91 | return nil, ErrUnmappedData | ||
| 90 | } | 92 | } | ||
| 91 | - | // Slice the raw memory as []float32 for this channel. | 93 | + | // Reject if the buffer cannot hold the requested number of frames. |
| 92 | - | // Data.Maxsize gives the total buffer size in bytes for this channel. | 94 | + | maxFrames := int(data.Maxsize) / 4 |
| 93 | - | nFloats := frames | 95 | + | if frames > maxFrames { |
| 94 | - | if maxSize := int(data.Maxsize) / 4; nFloats > maxSize { | 96 | + | return nil, fmt.Errorf("%w: requested %d frames but buffer holds %d", ErrFrameOverflow, frames, maxFrames) |
| 95 | - | nFloats = maxSize | |||
| 96 | } | 97 | } | ||
| 97 | - | samples[i] = unsafe.Slice((*float32)(data.Data), nFloats) | 98 | + | samples[i] = unsafe.Slice((*float32)(data.Data), frames) |
| 98 | } | 99 | } | ||
| 99 | 100 | ||||
| 100 | return &pwBufferView{ | 101 | return &pwBufferView{ | ||
| @@ -107,10 +108,12 @@ func newPWBufferView(bufPtr unsafe.Pointer, channels, frames int) (*pwBufferView | |||||
| 107 | 108 | ||||
| 108 | // PCM returns a PCMBuffer pointing at the buffer's planar float32 data. | 109 | // PCM returns a PCMBuffer pointing at the buffer's planar float32 data. | ||
| 109 | // The caller can write into Samples and then call Commit. | 110 | // The caller can write into Samples and then call Commit. | ||
| 111 | + | // Stride is set to 4 (bytes per float32 sample) for planar float32 layout. | |||
| 110 | func (v *pwBufferView) PCM() *PCMBuffer { | 112 | func (v *pwBufferView) PCM() *PCMBuffer { | ||
| 111 | return &PCMBuffer{ | 113 | return &PCMBuffer{ | ||
| 112 | Frames: v.frames, | 114 | Frames: v.frames, | ||
| 113 | Channels: v.channels, | 115 | Channels: v.channels, | ||
| 116 | + | Stride: 4, // sizeof(float32) — planar float32 byte stride per channel | |||
| 114 | Samples: v.samples, | 117 | Samples: v.samples, | ||
| 115 | } | 118 | } | ||
| 116 | } | 119 | } | ||
M internal/core/pwbuffer_test.go +110 -62
| @@ -6,62 +6,58 @@ import ( | |||
| 6 | 6 | "unsafe" | |
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | - | // TestNewPWBufferViewWrapsWritablePlanarFloatData verifies that newPWBufferView | |
| 10 | - | // correctly interprets a C-layout pwBuffer as planar float32 channel data. | |
| 11 | - | func TestNewPWBufferViewWrapsWritablePlanarFloatData(t *testing.T) { | |
| 12 | - | const channels = 2 | |
| 13 | - | const frames = 64 | |
| 9 | + | // Named constants for SPA data type and flag values (from <spa/buffer/buffer.h>). | |
| 10 | + | const ( | |
| 11 | + | spaDataTypeMemPtr uint32 = 2 // SPA_DATA_MemPtr | |
| 12 | + | spaDataFlagRW uint32 = 3 // SPA_DATA_FLAG_READABLE | SPA_DATA_FLAG_WRITABLE | |
| 13 | + | ) | |
| 14 | 14 | ||
| 15 | - | // Allocate planar float32 backing memory for each channel. | |
| 15 | + | // helper to build a channels×frames buffer view for tests. | |
| 16 | + | // Returns the view, the chunks slice, and the pwBuffer pointer. | |
| 17 | + | func newTestBufferView(t *testing.T, channels, frames, maxsizeFrames int) (*pwBufferView, []spaChunk, *pwBuffer) { | |
| 18 | + | t.Helper() | |
| 16 | 19 | channelData := make([][]float32, channels) | |
| 17 | 20 | channelPtrs := make([]unsafe.Pointer, channels) | |
| 18 | 21 | for ch := 0; ch < channels; ch++ { | |
| 19 | - | channelData[ch] = make([]float32, frames) | |
| 22 | + | channelData[ch] = make([]float32, maxsizeFrames) | |
| 20 | 23 | channelPtrs[ch] = unsafe.Pointer(&channelData[ch][0]) | |
| 21 | 24 | } | |
| 22 | 25 | ||
| 23 | - | // Build the C-layout chunk structs. | |
| 24 | 26 | chunks := make([]spaChunk, channels) | |
| 25 | - | for ch := 0; ch < channels; ch++ { | |
| 26 | - | chunks[ch] = spaChunk{ | |
| 27 | - | Offset: 0, | |
| 28 | - | Size: 0, // will be set on Commit | |
| 29 | - | Stride: 0, | |
| 30 | - | Flags: 0, | |
| 31 | - | } | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | // Build the C-layout spaData structs. | |
| 35 | 27 | datas := make([]spaData, channels) | |
| 36 | 28 | for ch := 0; ch < channels; ch++ { | |
| 29 | + | chunks[ch] = spaChunk{} | |
| 37 | 30 | datas[ch] = spaData{ | |
| 38 | - | Type: 2, // SPA_DATA_MemPtr | |
| 39 | - | Flags: 3, // SPA_DATA_FLAG_READABLE | SPA_DATA_FLAG_WRITABLE | |
| 40 | - | Maxsize: uint32(frames * 4), // bytes | |
| 31 | + | Type: spaDataTypeMemPtr, | |
| 32 | + | Flags: spaDataFlagRW, | |
| 33 | + | Maxsize: uint32(maxsizeFrames * 4), | |
| 41 | 34 | Data: channelPtrs[ch], | |
| 42 | 35 | Chunk: &chunks[ch], | |
| 43 | 36 | } | |
| 44 | 37 | } | |
| 45 | 38 | ||
| 46 | - | // Build the C-layout spaBuffer. | |
| 47 | 39 | buf := spaBuffer{ | |
| 48 | - | NMetas: 0, | |
| 49 | 40 | NDatas: uint32(channels), | |
| 50 | - | Metas: nil, | |
| 51 | 41 | Datas: unsafe.Pointer(&datas[0]), | |
| 52 | 42 | } | |
| 53 | - | ||
| 54 | - | // Build the C-layout pwBuffer. | |
| 55 | 43 | pwBuf := pwBuffer{ | |
| 56 | - | Buffer: &buf, | |
| 57 | - | UserData: nil, | |
| 58 | - | Size: 0, | |
| 44 | + | Buffer: &buf, | |
| 59 | 45 | } | |
| 60 | 46 | ||
| 61 | 47 | view, err := newPWBufferView(unsafe.Pointer(&pwBuf), channels, frames) | |
| 62 | 48 | if err != nil { | |
| 63 | - | t.Fatalf("unexpected error: %v", err) | |
| 49 | + | t.Fatalf("unexpected error building test buffer: %v", err) | |
| 64 | 50 | } | |
| 51 | + | return view, chunks, &pwBuf | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // TestNewPWBufferViewWrapsWritablePlanarFloatData verifies that newPWBufferView | |
| 55 | + | // correctly interprets a C-layout pwBuffer as planar float32 channel data. | |
| 56 | + | func TestNewPWBufferViewWrapsWritablePlanarFloatData(t *testing.T) { | |
| 57 | + | const channels = 2 | |
| 58 | + | const frames = 64 | |
| 59 | + | ||
| 60 | + | view, _, _ := newTestBufferView(t, channels, frames, frames) | |
| 65 | 61 | ||
| 66 | 62 | pcm := view.PCM() | |
| 67 | 63 | if pcm.Channels != channels { | |
| @@ -70,6 +66,9 @@ func TestNewPWBufferViewWrapsWritablePlanarFloatData(t *testing.T) { | |||
| 70 | 66 | if pcm.Frames != frames { | |
| 71 | 67 | t.Errorf("expected %d frames, got %d", frames, pcm.Frames) | |
| 72 | 68 | } | |
| 69 | + | if pcm.Stride != 4 { | |
| 70 | + | t.Errorf("expected Stride=4 for planar float32, got %d", pcm.Stride) | |
| 71 | + | } | |
| 73 | 72 | if len(pcm.Samples) != channels { | |
| 74 | 73 | t.Fatalf("expected %d sample slices, got %d", channels, len(pcm.Samples)) | |
| 75 | 74 | } | |
| @@ -78,8 +77,60 @@ func TestNewPWBufferViewWrapsWritablePlanarFloatData(t *testing.T) { | |||
| 78 | 77 | t.Errorf("channel %d: expected %d frames, got %d", ch, frames, len(pcm.Samples[ch])) | |
| 79 | 78 | } | |
| 80 | 79 | } | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // TestNewPWBufferViewPCMStrideIsSet verifies that PCM() returns a buffer | |
| 83 | + | // with Stride set to 4 (bytes per float32 for planar layout). | |
| 84 | + | func TestNewPWBufferViewPCMStrideIsSet(t *testing.T) { | |
| 85 | + | const channels = 1 | |
| 86 | + | const frames = 32 | |
| 87 | + | ||
| 88 | + | view, _, _ := newTestBufferView(t, channels, frames, frames) | |
| 89 | + | ||
| 90 | + | pcm := view.PCM() | |
| 91 | + | if pcm.Stride != 4 { | |
| 92 | + | t.Errorf("PCMBuffer.Stride = %d, want 4 (sizeof float32)", pcm.Stride) | |
| 93 | + | } | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | // TestNewPWBufferViewSharedMemory verifies that writes to the PCMBuffer | |
| 97 | + | // propagate to the underlying channel data (not a copy). | |
| 98 | + | func TestNewPWBufferViewSharedMemory(t *testing.T) { | |
| 99 | + | const channels = 2 | |
| 100 | + | const frames = 64 | |
| 101 | + | ||
| 102 | + | channelData := make([][]float32, channels) | |
| 103 | + | channelPtrs := make([]unsafe.Pointer, channels) | |
| 104 | + | for ch := 0; ch < channels; ch++ { | |
| 105 | + | channelData[ch] = make([]float32, frames) | |
| 106 | + | channelPtrs[ch] = unsafe.Pointer(&channelData[ch][0]) | |
| 107 | + | } | |
| 81 | 108 | ||
| 82 | - | // Verify the PCM buffer shares memory with channelData (not a copy). | |
| 109 | + | chunks := make([]spaChunk, channels) | |
| 110 | + | datas := make([]spaData, channels) | |
| 111 | + | for ch := 0; ch < channels; ch++ { | |
| 112 | + | chunks[ch] = spaChunk{} | |
| 113 | + | datas[ch] = spaData{ | |
| 114 | + | Type: spaDataTypeMemPtr, | |
| 115 | + | Flags: spaDataFlagRW, | |
| 116 | + | Maxsize: uint32(frames * 4), | |
| 117 | + | Data: channelPtrs[ch], | |
| 118 | + | Chunk: &chunks[ch], | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | buf := spaBuffer{ | |
| 123 | + | NDatas: uint32(channels), | |
| 124 | + | Datas: unsafe.Pointer(&datas[0]), | |
| 125 | + | } | |
| 126 | + | pwBuf := pwBuffer{Buffer: &buf} | |
| 127 | + | ||
| 128 | + | view, err := newPWBufferView(unsafe.Pointer(&pwBuf), channels, frames) | |
| 129 | + | if err != nil { | |
| 130 | + | t.Fatalf("unexpected error: %v", err) | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | pcm := view.PCM() | |
| 83 | 134 | pcm.Samples[0][0] = 1.0 | |
| 84 | 135 | if channelData[0][0] != 1.0 { | |
| 85 | 136 | t.Error("PCM buffer does not share memory with underlying channel data — write did not propagate") | |
| @@ -114,21 +165,17 @@ func TestNewPWBufferViewRejectsNilPointer(t *testing.T) { | |||
| 114 | 165 | // TestNewPWBufferViewRejectsChannelMismatch verifies that a buffer whose | |
| 115 | 166 | // NDatas does not match the requested channel count returns ErrChannelMismatch. | |
| 116 | 167 | func TestNewPWBufferViewRejectsChannelMismatch(t *testing.T) { | |
| 117 | - | // Build a buffer claiming 1 data slot. | |
| 118 | 168 | singleData := spaData{ | |
| 119 | - | Type: 2, | |
| 169 | + | Type: spaDataTypeMemPtr, | |
| 120 | 170 | Maxsize: 256, | |
| 121 | 171 | Data: unsafe.Pointer(new(float32)), | |
| 122 | 172 | Chunk: &spaChunk{}, | |
| 123 | 173 | } | |
| 124 | 174 | buf := spaBuffer{ | |
| 125 | - | NMetas: 0, | |
| 126 | 175 | NDatas: 1, // only 1 channel | |
| 127 | 176 | Datas: unsafe.Pointer(&singleData), | |
| 128 | 177 | } | |
| 129 | - | pwBuf := pwBuffer{ | |
| 130 | - | Buffer: &buf, | |
| 131 | - | } | |
| 178 | + | pwBuf := pwBuffer{Buffer: &buf} | |
| 132 | 179 | ||
| 133 | 180 | _, err := newPWBufferView(unsafe.Pointer(&pwBuf), 2, 64) // requesting 2 channels | |
| 134 | 181 | if !errors.Is(err, ErrChannelMismatch) { | |
| @@ -141,26 +188,23 @@ func TestNewPWBufferViewRejectsChannelMismatch(t *testing.T) { | |||
| 141 | 188 | func TestNewPWBufferViewRejectsUnmappedData(t *testing.T) { | |
| 142 | 189 | datas := make([]spaData, 2) | |
| 143 | 190 | datas[0] = spaData{ | |
| 144 | - | Type: 2, | |
| 191 | + | Type: spaDataTypeMemPtr, | |
| 145 | 192 | Maxsize: 256, | |
| 146 | 193 | Data: nil, // nil — should trigger ErrUnmappedData | |
| 147 | 194 | Chunk: &spaChunk{}, | |
| 148 | 195 | } | |
| 149 | 196 | datas[1] = spaData{ | |
| 150 | - | Type: 2, | |
| 197 | + | Type: spaDataTypeMemPtr, | |
| 151 | 198 | Maxsize: 256, | |
| 152 | 199 | Data: unsafe.Pointer(new(float32)), | |
| 153 | 200 | Chunk: &spaChunk{}, | |
| 154 | 201 | } | |
| 155 | 202 | ||
| 156 | 203 | buf := spaBuffer{ | |
| 157 | - | NMetas: 0, | |
| 158 | 204 | NDatas: 2, | |
| 159 | 205 | Datas: unsafe.Pointer(&datas[0]), | |
| 160 | 206 | } | |
| 161 | - | pwBuf := pwBuffer{ | |
| 162 | - | Buffer: &buf, | |
| 163 | - | } | |
| 207 | + | pwBuf := pwBuffer{Buffer: &buf} | |
| 164 | 208 | ||
| 165 | 209 | _, err := newPWBufferView(unsafe.Pointer(&pwBuf), 2, 64) | |
| 166 | 210 | if !errors.Is(err, ErrUnmappedData) { | |
| @@ -172,9 +216,7 @@ func TestNewPWBufferViewRejectsUnmappedData(t *testing.T) { | |||
| 172 | 216 | // pointer inside the pwBuffer returns ErrNilBufferPointer. | |
| 173 | 217 | func TestNewPWBufferViewRejectsNilSpaBuffer(t *testing.T) { | |
| 174 | 218 | pwBuf := pwBuffer{ | |
| 175 | - | Buffer: nil, // nil — should trigger ErrNilBufferPointer | |
| 176 | - | UserData: nil, | |
| 177 | - | Size: 0, | |
| 219 | + | Buffer: nil, | |
| 178 | 220 | } | |
| 179 | 221 | ||
| 180 | 222 | _, err := newPWBufferView(unsafe.Pointer(&pwBuf), 2, 64) | |
| @@ -183,43 +225,49 @@ func TestNewPWBufferViewRejectsNilSpaBuffer(t *testing.T) { | |||
| 183 | 225 | } | |
| 184 | 226 | } | |
| 185 | 227 | ||
| 186 | - | // TestCommitUpdatesSizeAndStride verifies that Commit writes the correct | |
| 187 | - | // byte sizes and strides for planar float32 data. | |
| 188 | - | func TestCommitUpdatesSizeAndStride(t *testing.T) { | |
| 228 | + | // TestNewPWBufferViewRejectsOversizedFrames verifies that requesting more | |
| 229 | + | // frames than the buffer can hold (based on Maxsize) returns ErrFrameOverflow. | |
| 230 | + | func TestNewPWBufferViewRejectsOversizedFrames(t *testing.T) { | |
| 189 | 231 | const channels = 2 | |
| 190 | - | const frames = 128 | |
| 232 | + | const maxFrames = 32 | |
| 233 | + | const requestedFrames = 64 // exceeds maxFrames | |
| 191 | 234 | ||
| 192 | 235 | channelData := make([][]float32, channels) | |
| 193 | 236 | channelPtrs := make([]unsafe.Pointer, channels) | |
| 194 | 237 | for ch := 0; ch < channels; ch++ { | |
| 195 | - | channelData[ch] = make([]float32, frames) | |
| 238 | + | channelData[ch] = make([]float32, maxFrames) | |
| 196 | 239 | channelPtrs[ch] = unsafe.Pointer(&channelData[ch][0]) | |
| 197 | 240 | } | |
| 198 | 241 | ||
| 199 | - | chunks := make([]spaChunk, channels) | |
| 200 | 242 | datas := make([]spaData, channels) | |
| 201 | 243 | for ch := 0; ch < channels; ch++ { | |
| 202 | 244 | datas[ch] = spaData{ | |
| 203 | - | Type: 2, | |
| 204 | - | Maxsize: uint32(frames * 4), | |
| 245 | + | Type: spaDataTypeMemPtr, | |
| 246 | + | Maxsize: uint32(maxFrames * 4), // only room for 32 frames | |
| 205 | 247 | Data: channelPtrs[ch], | |
| 206 | - | Chunk: &chunks[ch], | |
| 248 | + | Chunk: &spaChunk{}, | |
| 207 | 249 | } | |
| 208 | 250 | } | |
| 209 | 251 | ||
| 210 | 252 | buf := spaBuffer{ | |
| 211 | - | NMetas: 0, | |
| 212 | 253 | NDatas: uint32(channels), | |
| 213 | 254 | Datas: unsafe.Pointer(&datas[0]), | |
| 214 | 255 | } | |
| 215 | - | pwBuf := pwBuffer{ | |
| 216 | - | Buffer: &buf, | |
| 217 | - | } | |
| 256 | + | pwBuf := pwBuffer{Buffer: &buf} | |
| 218 | 257 | ||
| 219 | - | view, err := newPWBufferView(unsafe.Pointer(&pwBuf), channels, frames) | |
| 220 | - | if err != nil { | |
| 221 | - | t.Fatalf("unexpected error: %v", err) | |
| 258 | + | _, err := newPWBufferView(unsafe.Pointer(&pwBuf), channels, requestedFrames) | |
| 259 | + | if !errors.Is(err, ErrFrameOverflow) { | |
| 260 | + | t.Fatalf("expected ErrFrameOverflow, got %v", err) | |
| 222 | 261 | } | |
| 262 | + | } | |
| 263 | + | ||
| 264 | + | // TestCommitUpdatesSizeAndStride verifies that Commit writes the correct | |
| 265 | + | // byte sizes and strides for planar float32 data. | |
| 266 | + | func TestCommitUpdatesSizeAndStride(t *testing.T) { | |
| 267 | + | const channels = 2 | |
| 268 | + | const frames = 128 | |
| 269 | + | ||
| 270 | + | view, chunks, pwBuf := newTestBufferView(t, channels, frames, frames) | |
| 223 | 271 | ||
| 224 | 272 | // Write some audio data to the PCM buffer. | |
| 225 | 273 | const writtenFrames = 96 | |
| @@ -6,62 +6,58 @@ import ( | |||||
| 6 | "unsafe" | 6 | "unsafe" | ||
| 7 | ) | 7 | ) | ||
| 8 | 8 | ||||
| 9 | - | // TestNewPWBufferViewWrapsWritablePlanarFloatData verifies that newPWBufferView | 9 | + | // Named constants for SPA data type and flag values (from <spa/buffer/buffer.h>). |
| 10 | - | // correctly interprets a C-layout pwBuffer as planar float32 channel data. | 10 | + | const ( |
| 11 | - | func TestNewPWBufferViewWrapsWritablePlanarFloatData(t *testing.T) { | 11 | + | spaDataTypeMemPtr uint32 = 2 // SPA_DATA_MemPtr |
| 12 | - | const channels = 2 | 12 | + | spaDataFlagRW uint32 = 3 // SPA_DATA_FLAG_READABLE | SPA_DATA_FLAG_WRITABLE |
| 13 | - | const frames = 64 | 13 | + | ) |
| 14 | 14 | ||||
| 15 | - | // Allocate planar float32 backing memory for each channel. | 15 | + | // helper to build a channels×frames buffer view for tests. |
| 16 | + | // Returns the view, the chunks slice, and the pwBuffer pointer. | |||
| 17 | + | func newTestBufferView(t *testing.T, channels, frames, maxsizeFrames int) (*pwBufferView, []spaChunk, *pwBuffer) { | |||
| 18 | + | t.Helper() | |||
| 16 | channelData := make([][]float32, channels) | 19 | channelData := make([][]float32, channels) | ||
| 17 | channelPtrs := make([]unsafe.Pointer, channels) | 20 | channelPtrs := make([]unsafe.Pointer, channels) | ||
| 18 | for ch := 0; ch < channels; ch++ { | 21 | for ch := 0; ch < channels; ch++ { | ||
| 19 | - | channelData[ch] = make([]float32, frames) | 22 | + | channelData[ch] = make([]float32, maxsizeFrames) |
| 20 | channelPtrs[ch] = unsafe.Pointer(&channelData[ch][0]) | 23 | channelPtrs[ch] = unsafe.Pointer(&channelData[ch][0]) | ||
| 21 | } | 24 | } | ||
| 22 | 25 | ||||
| 23 | - | // Build the C-layout chunk structs. | |||
| 24 | chunks := make([]spaChunk, channels) | 26 | chunks := make([]spaChunk, channels) | ||
| 25 | - | for ch := 0; ch < channels; ch++ { | |||
| 26 | - | chunks[ch] = spaChunk{ | |||
| 27 | - | Offset: 0, | |||
| 28 | - | Size: 0, // will be set on Commit | |||
| 29 | - | Stride: 0, | |||
| 30 | - | Flags: 0, | |||
| 31 | - | } | |||
| 32 | - | } | |||
| 33 | - | ||||
| 34 | - | // Build the C-layout spaData structs. | |||
| 35 | datas := make([]spaData, channels) | 27 | datas := make([]spaData, channels) | ||
| 36 | for ch := 0; ch < channels; ch++ { | 28 | for ch := 0; ch < channels; ch++ { | ||
| 29 | + | chunks[ch] = spaChunk{} | |||
| 37 | datas[ch] = spaData{ | 30 | datas[ch] = spaData{ | ||
| 38 | - | Type: 2, // SPA_DATA_MemPtr | 31 | + | Type: spaDataTypeMemPtr, |
| 39 | - | Flags: 3, // SPA_DATA_FLAG_READABLE | SPA_DATA_FLAG_WRITABLE | 32 | + | Flags: spaDataFlagRW, |
| 40 | - | Maxsize: uint32(frames * 4), // bytes | 33 | + | Maxsize: uint32(maxsizeFrames * 4), |
| 41 | Data: channelPtrs[ch], | 34 | Data: channelPtrs[ch], | ||
| 42 | Chunk: &chunks[ch], | 35 | Chunk: &chunks[ch], | ||
| 43 | } | 36 | } | ||
| 44 | } | 37 | } | ||
| 45 | 38 | ||||
| 46 | - | // Build the C-layout spaBuffer. | |||
| 47 | buf := spaBuffer{ | 39 | buf := spaBuffer{ | ||
| 48 | - | NMetas: 0, | |||
| 49 | NDatas: uint32(channels), | 40 | NDatas: uint32(channels), | ||
| 50 | - | Metas: nil, | |||
| 51 | Datas: unsafe.Pointer(&datas[0]), | 41 | Datas: unsafe.Pointer(&datas[0]), | ||
| 52 | } | 42 | } | ||
| 53 | - | ||||
| 54 | - | // Build the C-layout pwBuffer. | |||
| 55 | pwBuf := pwBuffer{ | 43 | pwBuf := pwBuffer{ | ||
| 56 | - | Buffer: &buf, | 44 | + | Buffer: &buf, |
| 57 | - | UserData: nil, | |||
| 58 | - | Size: 0, | |||
| 59 | } | 45 | } | ||
| 60 | 46 | ||||
| 61 | view, err := newPWBufferView(unsafe.Pointer(&pwBuf), channels, frames) | 47 | view, err := newPWBufferView(unsafe.Pointer(&pwBuf), channels, frames) | ||
| 62 | if err != nil { | 48 | if err != nil { | ||
| 63 | - | t.Fatalf("unexpected error: %v", err) | 49 | + | t.Fatalf("unexpected error building test buffer: %v", err) |
| 64 | } | 50 | } | ||
| 51 | + | return view, chunks, &pwBuf | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | // TestNewPWBufferViewWrapsWritablePlanarFloatData verifies that newPWBufferView | |||
| 55 | + | // correctly interprets a C-layout pwBuffer as planar float32 channel data. | |||
| 56 | + | func TestNewPWBufferViewWrapsWritablePlanarFloatData(t *testing.T) { | |||
| 57 | + | const channels = 2 | |||
| 58 | + | const frames = 64 | |||
| 59 | + | ||||
| 60 | + | view, _, _ := newTestBufferView(t, channels, frames, frames) | |||
| 65 | 61 | ||||
| 66 | pcm := view.PCM() | 62 | pcm := view.PCM() | ||
| 67 | if pcm.Channels != channels { | 63 | if pcm.Channels != channels { | ||
| @@ -70,6 +66,9 @@ func TestNewPWBufferViewWrapsWritablePlanarFloatData(t *testing.T) { | |||||
| 70 | if pcm.Frames != frames { | 66 | if pcm.Frames != frames { | ||
| 71 | t.Errorf("expected %d frames, got %d", frames, pcm.Frames) | 67 | t.Errorf("expected %d frames, got %d", frames, pcm.Frames) | ||
| 72 | } | 68 | } | ||
| 69 | + | if pcm.Stride != 4 { | |||
| 70 | + | t.Errorf("expected Stride=4 for planar float32, got %d", pcm.Stride) | |||
| 71 | + | } | |||
| 73 | if len(pcm.Samples) != channels { | 72 | if len(pcm.Samples) != channels { | ||
| 74 | t.Fatalf("expected %d sample slices, got %d", channels, len(pcm.Samples)) | 73 | t.Fatalf("expected %d sample slices, got %d", channels, len(pcm.Samples)) | ||
| 75 | } | 74 | } | ||
| @@ -78,8 +77,60 @@ func TestNewPWBufferViewWrapsWritablePlanarFloatData(t *testing.T) { | |||||
| 78 | t.Errorf("channel %d: expected %d frames, got %d", ch, frames, len(pcm.Samples[ch])) | 77 | t.Errorf("channel %d: expected %d frames, got %d", ch, frames, len(pcm.Samples[ch])) | ||
| 79 | } | 78 | } | ||
| 80 | } | 79 | } | ||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | // TestNewPWBufferViewPCMStrideIsSet verifies that PCM() returns a buffer | |||
| 83 | + | // with Stride set to 4 (bytes per float32 for planar layout). | |||
| 84 | + | func TestNewPWBufferViewPCMStrideIsSet(t *testing.T) { | |||
| 85 | + | const channels = 1 | |||
| 86 | + | const frames = 32 | |||
| 87 | + | ||||
| 88 | + | view, _, _ := newTestBufferView(t, channels, frames, frames) | |||
| 89 | + | ||||
| 90 | + | pcm := view.PCM() | |||
| 91 | + | if pcm.Stride != 4 { | |||
| 92 | + | t.Errorf("PCMBuffer.Stride = %d, want 4 (sizeof float32)", pcm.Stride) | |||
| 93 | + | } | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | // TestNewPWBufferViewSharedMemory verifies that writes to the PCMBuffer | |||
| 97 | + | // propagate to the underlying channel data (not a copy). | |||
| 98 | + | func TestNewPWBufferViewSharedMemory(t *testing.T) { | |||
| 99 | + | const channels = 2 | |||
| 100 | + | const frames = 64 | |||
| 101 | + | ||||
| 102 | + | channelData := make([][]float32, channels) | |||
| 103 | + | channelPtrs := make([]unsafe.Pointer, channels) | |||
| 104 | + | for ch := 0; ch < channels; ch++ { | |||
| 105 | + | channelData[ch] = make([]float32, frames) | |||
| 106 | + | channelPtrs[ch] = unsafe.Pointer(&channelData[ch][0]) | |||
| 107 | + | } | |||
| 81 | 108 | ||||
| 82 | - | // Verify the PCM buffer shares memory with channelData (not a copy). | 109 | + | chunks := make([]spaChunk, channels) |
| 110 | + | datas := make([]spaData, channels) | |||
| 111 | + | for ch := 0; ch < channels; ch++ { | |||
| 112 | + | chunks[ch] = spaChunk{} | |||
| 113 | + | datas[ch] = spaData{ | |||
| 114 | + | Type: spaDataTypeMemPtr, | |||
| 115 | + | Flags: spaDataFlagRW, | |||
| 116 | + | Maxsize: uint32(frames * 4), | |||
| 117 | + | Data: channelPtrs[ch], | |||
| 118 | + | Chunk: &chunks[ch], | |||
| 119 | + | } | |||
| 120 | + | } | |||
| 121 | + | ||||
| 122 | + | buf := spaBuffer{ | |||
| 123 | + | NDatas: uint32(channels), | |||
| 124 | + | Datas: unsafe.Pointer(&datas[0]), | |||
| 125 | + | } | |||
| 126 | + | pwBuf := pwBuffer{Buffer: &buf} | |||
| 127 | + | ||||
| 128 | + | view, err := newPWBufferView(unsafe.Pointer(&pwBuf), channels, frames) | |||
| 129 | + | if err != nil { | |||
| 130 | + | t.Fatalf("unexpected error: %v", err) | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | pcm := view.PCM() | |||
| 83 | pcm.Samples[0][0] = 1.0 | 134 | pcm.Samples[0][0] = 1.0 | ||
| 84 | if channelData[0][0] != 1.0 { | 135 | if channelData[0][0] != 1.0 { | ||
| 85 | t.Error("PCM buffer does not share memory with underlying channel data — write did not propagate") | 136 | t.Error("PCM buffer does not share memory with underlying channel data — write did not propagate") | ||
| @@ -114,21 +165,17 @@ func TestNewPWBufferViewRejectsNilPointer(t *testing.T) { | |||||
| 114 | // TestNewPWBufferViewRejectsChannelMismatch verifies that a buffer whose | 165 | // TestNewPWBufferViewRejectsChannelMismatch verifies that a buffer whose | ||
| 115 | // NDatas does not match the requested channel count returns ErrChannelMismatch. | 166 | // NDatas does not match the requested channel count returns ErrChannelMismatch. | ||
| 116 | func TestNewPWBufferViewRejectsChannelMismatch(t *testing.T) { | 167 | func TestNewPWBufferViewRejectsChannelMismatch(t *testing.T) { | ||
| 117 | - | // Build a buffer claiming 1 data slot. | |||
| 118 | singleData := spaData{ | 168 | singleData := spaData{ | ||
| 119 | - | Type: 2, | 169 | + | Type: spaDataTypeMemPtr, |
| 120 | Maxsize: 256, | 170 | Maxsize: 256, | ||
| 121 | Data: unsafe.Pointer(new(float32)), | 171 | Data: unsafe.Pointer(new(float32)), | ||
| 122 | Chunk: &spaChunk{}, | 172 | Chunk: &spaChunk{}, | ||
| 123 | } | 173 | } | ||
| 124 | buf := spaBuffer{ | 174 | buf := spaBuffer{ | ||
| 125 | - | NMetas: 0, | |||
| 126 | NDatas: 1, // only 1 channel | 175 | NDatas: 1, // only 1 channel | ||
| 127 | Datas: unsafe.Pointer(&singleData), | 176 | Datas: unsafe.Pointer(&singleData), | ||
| 128 | } | 177 | } | ||
| 129 | - | pwBuf := pwBuffer{ | 178 | + | pwBuf := pwBuffer{Buffer: &buf} |
| 130 | - | Buffer: &buf, | |||
| 131 | - | } | |||
| 132 | 179 | ||||
| 133 | _, err := newPWBufferView(unsafe.Pointer(&pwBuf), 2, 64) // requesting 2 channels | 180 | _, err := newPWBufferView(unsafe.Pointer(&pwBuf), 2, 64) // requesting 2 channels | ||
| 134 | if !errors.Is(err, ErrChannelMismatch) { | 181 | if !errors.Is(err, ErrChannelMismatch) { | ||
| @@ -141,26 +188,23 @@ func TestNewPWBufferViewRejectsChannelMismatch(t *testing.T) { | |||||
| 141 | func TestNewPWBufferViewRejectsUnmappedData(t *testing.T) { | 188 | func TestNewPWBufferViewRejectsUnmappedData(t *testing.T) { | ||
| 142 | datas := make([]spaData, 2) | 189 | datas := make([]spaData, 2) | ||
| 143 | datas[0] = spaData{ | 190 | datas[0] = spaData{ | ||
| 144 | - | Type: 2, | 191 | + | Type: spaDataTypeMemPtr, |
| 145 | Maxsize: 256, | 192 | Maxsize: 256, | ||
| 146 | Data: nil, // nil — should trigger ErrUnmappedData | 193 | Data: nil, // nil — should trigger ErrUnmappedData | ||
| 147 | Chunk: &spaChunk{}, | 194 | Chunk: &spaChunk{}, | ||
| 148 | } | 195 | } | ||
| 149 | datas[1] = spaData{ | 196 | datas[1] = spaData{ | ||
| 150 | - | Type: 2, | 197 | + | Type: spaDataTypeMemPtr, |
| 151 | Maxsize: 256, | 198 | Maxsize: 256, | ||
| 152 | Data: unsafe.Pointer(new(float32)), | 199 | Data: unsafe.Pointer(new(float32)), | ||
| 153 | Chunk: &spaChunk{}, | 200 | Chunk: &spaChunk{}, | ||
| 154 | } | 201 | } | ||
| 155 | 202 | ||||
| 156 | buf := spaBuffer{ | 203 | buf := spaBuffer{ | ||
| 157 | - | NMetas: 0, | |||
| 158 | NDatas: 2, | 204 | NDatas: 2, | ||
| 159 | Datas: unsafe.Pointer(&datas[0]), | 205 | Datas: unsafe.Pointer(&datas[0]), | ||
| 160 | } | 206 | } | ||
| 161 | - | pwBuf := pwBuffer{ | 207 | + | pwBuf := pwBuffer{Buffer: &buf} |
| 162 | - | Buffer: &buf, | |||
| 163 | - | } | |||
| 164 | 208 | ||||
| 165 | _, err := newPWBufferView(unsafe.Pointer(&pwBuf), 2, 64) | 209 | _, err := newPWBufferView(unsafe.Pointer(&pwBuf), 2, 64) | ||
| 166 | if !errors.Is(err, ErrUnmappedData) { | 210 | if !errors.Is(err, ErrUnmappedData) { | ||
| @@ -172,9 +216,7 @@ func TestNewPWBufferViewRejectsUnmappedData(t *testing.T) { | |||||
| 172 | // pointer inside the pwBuffer returns ErrNilBufferPointer. | 216 | // pointer inside the pwBuffer returns ErrNilBufferPointer. | ||
| 173 | func TestNewPWBufferViewRejectsNilSpaBuffer(t *testing.T) { | 217 | func TestNewPWBufferViewRejectsNilSpaBuffer(t *testing.T) { | ||
| 174 | pwBuf := pwBuffer{ | 218 | pwBuf := pwBuffer{ | ||
| 175 | - | Buffer: nil, // nil — should trigger ErrNilBufferPointer | 219 | + | Buffer: nil, |
| 176 | - | UserData: nil, | |||
| 177 | - | Size: 0, | |||
| 178 | } | 220 | } | ||
| 179 | 221 | ||||
| 180 | _, err := newPWBufferView(unsafe.Pointer(&pwBuf), 2, 64) | 222 | _, err := newPWBufferView(unsafe.Pointer(&pwBuf), 2, 64) | ||
| @@ -183,43 +225,49 @@ func TestNewPWBufferViewRejectsNilSpaBuffer(t *testing.T) { | |||||
| 183 | } | 225 | } | ||
| 184 | } | 226 | } | ||
| 185 | 227 | ||||
| 186 | - | // TestCommitUpdatesSizeAndStride verifies that Commit writes the correct | 228 | + | // TestNewPWBufferViewRejectsOversizedFrames verifies that requesting more |
| 187 | - | // byte sizes and strides for planar float32 data. | 229 | + | // frames than the buffer can hold (based on Maxsize) returns ErrFrameOverflow. |
| 188 | - | func TestCommitUpdatesSizeAndStride(t *testing.T) { | 230 | + | func TestNewPWBufferViewRejectsOversizedFrames(t *testing.T) { |
| 189 | const channels = 2 | 231 | const channels = 2 | ||
| 190 | - | const frames = 128 | 232 | + | const maxFrames = 32 |
| 233 | + | const requestedFrames = 64 // exceeds maxFrames | |||
| 191 | 234 | ||||
| 192 | channelData := make([][]float32, channels) | 235 | channelData := make([][]float32, channels) | ||
| 193 | channelPtrs := make([]unsafe.Pointer, channels) | 236 | channelPtrs := make([]unsafe.Pointer, channels) | ||
| 194 | for ch := 0; ch < channels; ch++ { | 237 | for ch := 0; ch < channels; ch++ { | ||
| 195 | - | channelData[ch] = make([]float32, frames) | 238 | + | channelData[ch] = make([]float32, maxFrames) |
| 196 | channelPtrs[ch] = unsafe.Pointer(&channelData[ch][0]) | 239 | channelPtrs[ch] = unsafe.Pointer(&channelData[ch][0]) | ||
| 197 | } | 240 | } | ||
| 198 | 241 | ||||
| 199 | - | chunks := make([]spaChunk, channels) | |||
| 200 | datas := make([]spaData, channels) | 242 | datas := make([]spaData, channels) | ||
| 201 | for ch := 0; ch < channels; ch++ { | 243 | for ch := 0; ch < channels; ch++ { | ||
| 202 | datas[ch] = spaData{ | 244 | datas[ch] = spaData{ | ||
| 203 | - | Type: 2, | 245 | + | Type: spaDataTypeMemPtr, |
| 204 | - | Maxsize: uint32(frames * 4), | 246 | + | Maxsize: uint32(maxFrames * 4), // only room for 32 frames |
| 205 | Data: channelPtrs[ch], | 247 | Data: channelPtrs[ch], | ||
| 206 | - | Chunk: &chunks[ch], | 248 | + | Chunk: &spaChunk{}, |
| 207 | } | 249 | } | ||
| 208 | } | 250 | } | ||
| 209 | 251 | ||||
| 210 | buf := spaBuffer{ | 252 | buf := spaBuffer{ | ||
| 211 | - | NMetas: 0, | |||
| 212 | NDatas: uint32(channels), | 253 | NDatas: uint32(channels), | ||
| 213 | Datas: unsafe.Pointer(&datas[0]), | 254 | Datas: unsafe.Pointer(&datas[0]), | ||
| 214 | } | 255 | } | ||
| 215 | - | pwBuf := pwBuffer{ | 256 | + | pwBuf := pwBuffer{Buffer: &buf} |
| 216 | - | Buffer: &buf, | |||
| 217 | - | } | |||
| 218 | 257 | ||||
| 219 | - | view, err := newPWBufferView(unsafe.Pointer(&pwBuf), channels, frames) | 258 | + | _, err := newPWBufferView(unsafe.Pointer(&pwBuf), channels, requestedFrames) |
| 220 | - | if err != nil { | 259 | + | if !errors.Is(err, ErrFrameOverflow) { |
| 221 | - | t.Fatalf("unexpected error: %v", err) | 260 | + | t.Fatalf("expected ErrFrameOverflow, got %v", err) |
| 222 | } | 261 | } | ||
| 262 | + | } | |||
| 263 | + | ||||
| 264 | + | // TestCommitUpdatesSizeAndStride verifies that Commit writes the correct | |||
| 265 | + | // byte sizes and strides for planar float32 data. | |||
| 266 | + | func TestCommitUpdatesSizeAndStride(t *testing.T) { | |||
| 267 | + | const channels = 2 | |||
| 268 | + | const frames = 128 | |||
| 269 | + | ||||
| 270 | + | view, chunks, pwBuf := newTestBufferView(t, channels, frames, frames) | |||
| 223 | 271 | ||||
| 224 | // Write some audio data to the PCM buffer. | 272 | // Write some audio data to the PCM buffer. | ||
| 225 | const writtenFrames = 96 | 273 | const writtenFrames = 96 | ||