detached 4c97002

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 8

↑↓ moveenter openesc close