pwbuffer.go RawHistoryFolder
1package core
2
3import (
4 "errors"
5 "fmt"
6 "unsafe"
7)
8
9// PipeWire buffer layout structs matching the C ABI.
10// These are low-level mappings that allow interpreting a dequeue'd buffer
11// pointer as writable planar float32 data. They are intentionally minimal
12// and local — not a general SPA buffer parser.
13
14// spaChunk mirrors struct spa_chunk from <spa/buffer/buffer.h>.
15type spaChunk struct {
16 Offset uint32
17 Size uint32
18 Stride int32
19 Flags int32
20}
21
22// spaData mirrors struct spa_data from <spa/buffer/buffer.h>.
23type spaData struct {
24 Type uint32
25 Flags uint32
26 Fd int64
27 MapOffset uint32
28 Maxsize uint32
29 Data unsafe.Pointer
30 Chunk *spaChunk
31}
32
33// spaBuffer mirrors struct spa_buffer from <spa/buffer/buffer.h>.
34type spaBuffer struct {
35 NMetas uint32
36 NDatas uint32
37 Metas unsafe.Pointer // struct spa_meta * — pointer to array of spa_meta
38 Datas unsafe.Pointer // struct spa_data * — pointer to array of spa_data
39}
40
41// pwBuffer mirrors struct pw_buffer from <pipewire/stream.h>.
42type pwBuffer struct {
43 Buffer *spaBuffer
44 UserData unsafe.Pointer
45 Size uint64
46 Requested uint64
47 Time uint64
48}
49
50// pwBufferView provides a safe Go view over a dequeue'd PipeWire buffer,
51// exposing planar float32 channel data. Call Commit after writing frames
52// to update chunk size/stride and pw buffer size.
53type pwBufferView struct {
54 buf *pwBuffer
55 channels int
56 frames int
57 data []spaData
58 pcm PCMBuffer
59 samples [][]float32
60}
61
62var (
63 ErrNilBufferPointer = errors.New("nil buffer pointer")
64 ErrChannelMismatch = errors.New("buffer data count does not match channels")
65 ErrUnmappedData = errors.New("buffer data pointer is nil")
66 ErrFrameOverflow = errors.New("requested frames exceed buffer capacity")
67)
68
69// newReusablePWBufferView allocates the Go slice headers once per player.
70// refresh replaces their native backing pointers for each dequeued buffer.
71func newReusablePWBufferView(channels, frames int) pwBufferView {
72 // Preserve constructor behavior for invalid channel counts: refresh will
73 // reject them against the native metadata instead of panicking in make.
74 var samples [][]float32
75 if channels > 0 {
76 samples = make([][]float32, channels)
77 }
78 return pwBufferView{
79 channels: channels,
80 frames: frames,
81 pcm: PCMBuffer{
82 Frames: frames,
83 Channels: channels,
84 Stride: 4,
85 Samples: samples,
86 },
87 samples: samples,
88 }
89}
90
91// newPWBufferView interprets bufPtr as a *pwBuffer and returns a view that
92// exposes planar float32 samples for each channel. It is retained for callers
93// that need a standalone view; the realtime player instead reuses one view.
94func newPWBufferView(bufPtr unsafe.Pointer, channels, frames int) (*pwBufferView, error) {
95 view := newReusablePWBufferView(channels, frames)
96 if err := view.refresh(bufPtr); err != nil {
97 return nil, err
98 }
99 return &view, nil
100}
101
102// refresh updates a reusable view from a dequeued native buffer. It clears all
103// native references before validation, so invalid metadata can never leave a
104// later callback writing through stale pointers.
105func (v *pwBufferView) refresh(bufPtr unsafe.Pointer) error {
106 v.clear()
107 if bufPtr == nil {
108 return ErrNilBufferPointer
109 }
110
111 pw := (*pwBuffer)(bufPtr)
112 if pw.Buffer == nil {
113 return ErrNilBufferPointer
114 }
115 if pw.Buffer.NDatas != uint32(v.channels) {
116 return ErrChannelMismatch
117 }
118 if v.channels > 0 && pw.Buffer.Datas == nil {
119 return ErrUnmappedData
120 }
121
122 // Datas is a pointer to a C array of spaData structs. unsafe.Slice creates
123 // only a slice header; it does not copy or allocate the native metadata.
124 dataSlice := unsafe.Slice((*spaData)(pw.Buffer.Datas), int(pw.Buffer.NDatas))
125 for i := 0; i < v.channels; i++ {
126 data := &dataSlice[i]
127 if data.Data == nil {
128 v.clear()
129 return ErrUnmappedData
130 }
131 maxFrames := int(data.Maxsize) / 4
132 if v.frames > maxFrames {
133 v.clear()
134 return fmt.Errorf("%w: requested %d frames but buffer holds %d", ErrFrameOverflow, v.frames, maxFrames)
135 }
136 // The length and capacity intentionally match the configured callback
137 // frame count, preserving the previous view contract even if PipeWire
138 // supplied a larger mapped buffer.
139 v.samples[i] = unsafe.Slice((*float32)(data.Data), v.frames)
140 }
141 v.buf = pw
142 v.data = dataSlice
143 return nil
144}
145
146func (v *pwBufferView) clear() {
147 v.buf = nil
148 v.data = nil
149 for i := range v.samples {
150 v.samples[i] = nil
151 }
152}
153
154// PCM returns a PCMBuffer pointing at the buffer's planar float32 data.
155// The caller can write into Samples and then call Commit.
156// Stride is set to 4 (bytes per float32 sample) for planar float32 layout.
157func (v *pwBufferView) PCM() *PCMBuffer {
158 return &v.pcm
159}
160
161// Commit updates each data chunk's size and stride to reflect the number of
162// frames written, and sets the pw buffer size field. Call this after writing
163// audio data into the PCM buffer.
164func (v *pwBufferView) Commit(frames int) {
165 v.buf.Size = uint64(frames)
166 for i := 0; i < v.channels; i++ {
167 data := &v.data[i]
168 if data.Chunk != nil {
169 data.Chunk.Size = uint32(frames * 4) // float32 = 4 bytes
170 data.Chunk.Stride = 4 // stride for planar float32
171 }
172 }
173}