Skip to content

Commit 770ebf2

Browse files
committed
init for a second memory share attempt
1 parent a96c4b4 commit 770ebf2

8 files changed

Lines changed: 1871 additions & 1 deletion

Source/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ target_sources(${PLUGIN_NAME} PRIVATE Config.h
1010
PannerOSC.h
1111
PannerOSC.cpp
1212
RingBuffer.h
13+
StreamingMode.h
14+
LockFreeRingBuffer.h
15+
M1SystemHelperManager.h
1316
WindowUtil.h
1417
WindowUtil.cpp
1518
UI/M1Label.h

Source/LockFreeRingBuffer.h

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
#pragma once
2+
3+
#include <atomic>
4+
#include <cstdint>
5+
#include <cstring>
6+
#include <algorithm>
7+
8+
/**
9+
* Lock-free Single-Producer Single-Consumer (SPSC) Ring Buffer
10+
*
11+
* Designed for real-time audio streaming between processes via memory-mapped files.
12+
*
13+
* Features:
14+
* - No locks, no allocations in read/write paths
15+
* - Cache-line padding to prevent false sharing
16+
* - Supports multichannel audio with interleaved storage
17+
* - Includes metadata header for synchronization
18+
*
19+
* Memory Layout:
20+
* [StreamHeader][padding][audio samples...]
21+
*/
22+
23+
namespace Mach1 {
24+
25+
// Cache line size for padding (typical x86/ARM)
26+
constexpr size_t CACHE_LINE_SIZE = 64;
27+
28+
/**
29+
* Stream header stored at the beginning of shared memory
30+
* Aligned and padded to prevent false sharing between producer/consumer
31+
*/
32+
struct alignas(CACHE_LINE_SIZE) StreamHeader {
33+
// Magic number to verify valid stream
34+
static constexpr uint32_t MAGIC = 0x4D315354; // "M1ST"
35+
static constexpr uint32_t VERSION = 1;
36+
37+
// Immutable after initialization
38+
uint32_t magic = MAGIC;
39+
uint32_t version = VERSION;
40+
uint32_t numChannels = 0;
41+
uint32_t bufferCapacitySamples = 0; // Per-channel capacity in samples
42+
uint32_t sampleRate = 0;
43+
uint32_t maxBlockSize = 0;
44+
45+
// Panner identification
46+
char pannerUuid[40] = {0}; // UUID string (36 chars + null)
47+
char sessionId[24] = {0}; // Host PID as string
48+
49+
// Mach1Encode coefficients (up to 14 channels output, 2 input channels for stereo)
50+
// Layout: gains[inputChannel][outputChannel]
51+
// For stereo input -> 14ch output: 2 * 14 = 28 floats
52+
float encodeGains[2][14] = {{0}};
53+
uint32_t encodeInputMode = 0;
54+
uint32_t encodeOutputMode = 0;
55+
uint32_t encodePannerMode = 0;
56+
57+
// Spatial parameters (for UI sync)
58+
float azimuth = 0.0f;
59+
float elevation = 0.0f;
60+
float diverge = 0.0f;
61+
float gain = 0.0f; // dB
62+
63+
// Stereo parameters
64+
uint32_t autoOrbit = 1;
65+
float stereoOrbitAzimuth = 0.0f;
66+
float stereoSpread = 0.5f;
67+
float stereoInputBalance = 0.0f;
68+
69+
// Transport info
70+
double playheadPositionSeconds = 0.0;
71+
uint32_t isPlaying = 0;
72+
uint64_t dawTimestamp = 0;
73+
74+
// Stream state
75+
uint32_t streamActive = 0;
76+
uint64_t lastWriteTimestamp = 0;
77+
78+
// Padding to separate from indices
79+
char _pad1[CACHE_LINE_SIZE - (sizeof(uint32_t) * 2 + sizeof(uint64_t) * 2) % CACHE_LINE_SIZE];
80+
81+
// Producer-owned index (written by producer, read by consumer)
82+
alignas(CACHE_LINE_SIZE) std::atomic<uint64_t> writeIndex{0};
83+
char _pad2[CACHE_LINE_SIZE - sizeof(std::atomic<uint64_t>)];
84+
85+
// Consumer-owned index (written by consumer, read by producer)
86+
alignas(CACHE_LINE_SIZE) std::atomic<uint64_t> readIndex{0};
87+
char _pad3[CACHE_LINE_SIZE - sizeof(std::atomic<uint64_t>)];
88+
89+
// Sequence number for detecting overwrites
90+
alignas(CACHE_LINE_SIZE) std::atomic<uint64_t> sequenceNumber{0};
91+
};
92+
93+
/**
94+
* Lock-free SPSC Ring Buffer for multichannel audio
95+
*
96+
* Audio is stored interleaved: [ch0_s0, ch1_s0, ch0_s1, ch1_s1, ...]
97+
*/
98+
class LockFreeRingBuffer {
99+
public:
100+
LockFreeRingBuffer() = default;
101+
~LockFreeRingBuffer() = default;
102+
103+
/**
104+
* Initialize buffer with existing memory region
105+
* @param memory Pointer to memory-mapped region
106+
* @param totalBytes Total size of memory region
107+
* @param numChannels Number of audio channels
108+
* @param capacitySamples Per-channel buffer capacity in samples
109+
* @param sampleRate Audio sample rate
110+
* @param maxBlockSize Maximum expected block size
111+
* @param initializeHeader If true, initialize header (producer). If false, just attach (consumer)
112+
* @return true if initialization successful
113+
*/
114+
bool initialize(void* memory, size_t totalBytes, uint32_t numChannels,
115+
uint32_t capacitySamples, uint32_t sampleRate, uint32_t maxBlockSize,
116+
bool initializeHeader) {
117+
if (!memory || totalBytes < getRequiredSize(numChannels, capacitySamples)) {
118+
return false;
119+
}
120+
121+
header = reinterpret_cast<StreamHeader*>(memory);
122+
audioData = reinterpret_cast<float*>(
123+
reinterpret_cast<uint8_t*>(memory) + sizeof(StreamHeader)
124+
);
125+
126+
this->numChannels = numChannels;
127+
this->capacitySamples = capacitySamples;
128+
129+
if (initializeHeader) {
130+
// Producer initializes the header
131+
std::memset(header, 0, sizeof(StreamHeader));
132+
header->magic = StreamHeader::MAGIC;
133+
header->version = StreamHeader::VERSION;
134+
header->numChannels = numChannels;
135+
header->bufferCapacitySamples = capacitySamples;
136+
header->sampleRate = sampleRate;
137+
header->maxBlockSize = maxBlockSize;
138+
header->writeIndex.store(0, std::memory_order_relaxed);
139+
header->readIndex.store(0, std::memory_order_relaxed);
140+
header->sequenceNumber.store(0, std::memory_order_relaxed);
141+
header->streamActive = 1;
142+
143+
// Zero audio buffer
144+
std::memset(audioData, 0, numChannels * capacitySamples * sizeof(float));
145+
} else {
146+
// Consumer verifies header
147+
if (header->magic != StreamHeader::MAGIC || header->version != StreamHeader::VERSION) {
148+
return false;
149+
}
150+
}
151+
152+
initialized = true;
153+
return true;
154+
}
155+
156+
/**
157+
* Attach to existing initialized buffer (consumer side)
158+
*/
159+
bool attach(void* memory, size_t totalBytes) {
160+
if (!memory || totalBytes < sizeof(StreamHeader)) {
161+
return false;
162+
}
163+
164+
header = reinterpret_cast<StreamHeader*>(memory);
165+
166+
// Verify magic and version
167+
if (header->magic != StreamHeader::MAGIC || header->version != StreamHeader::VERSION) {
168+
return false;
169+
}
170+
171+
numChannels = header->numChannels;
172+
capacitySamples = header->bufferCapacitySamples;
173+
174+
if (totalBytes < getRequiredSize(numChannels, capacitySamples)) {
175+
return false;
176+
}
177+
178+
audioData = reinterpret_cast<float*>(
179+
reinterpret_cast<uint8_t*>(memory) + sizeof(StreamHeader)
180+
);
181+
182+
initialized = true;
183+
return true;
184+
}
185+
186+
/**
187+
* Write multichannel audio block (producer only)
188+
* Non-blocking, overwrites oldest data if buffer is full
189+
*
190+
* @param channelData Array of channel pointers [numChannels][numSamples]
191+
* @param numSamples Number of samples to write per channel
192+
* @return true if write successful
193+
*/
194+
bool write(const float* const* channelData, uint32_t numSamples) {
195+
if (!initialized || !header || !audioData || numSamples == 0) {
196+
return false;
197+
}
198+
199+
const uint64_t writeIdx = header->writeIndex.load(std::memory_order_relaxed);
200+
201+
// Write interleaved samples
202+
for (uint32_t s = 0; s < numSamples; ++s) {
203+
const uint64_t ringPos = (writeIdx + s) % capacitySamples;
204+
const size_t baseIdx = ringPos * numChannels;
205+
206+
for (uint32_t ch = 0; ch < numChannels; ++ch) {
207+
audioData[baseIdx + ch] = channelData[ch][s];
208+
}
209+
}
210+
211+
// Update write index with release semantics (ensures audio data is visible)
212+
header->writeIndex.store(writeIdx + numSamples, std::memory_order_release);
213+
header->sequenceNumber.fetch_add(1, std::memory_order_relaxed);
214+
215+
return true;
216+
}
217+
218+
/**
219+
* Write single-channel audio and duplicate to all channels (producer only)
220+
*/
221+
bool writeMono(const float* monoData, uint32_t numSamples) {
222+
if (!initialized || !header || !audioData || numSamples == 0) {
223+
return false;
224+
}
225+
226+
const uint64_t writeIdx = header->writeIndex.load(std::memory_order_relaxed);
227+
228+
for (uint32_t s = 0; s < numSamples; ++s) {
229+
const uint64_t ringPos = (writeIdx + s) % capacitySamples;
230+
const size_t baseIdx = ringPos * numChannels;
231+
const float sample = monoData[s];
232+
233+
for (uint32_t ch = 0; ch < numChannels; ++ch) {
234+
audioData[baseIdx + ch] = sample;
235+
}
236+
}
237+
238+
header->writeIndex.store(writeIdx + numSamples, std::memory_order_release);
239+
header->sequenceNumber.fetch_add(1, std::memory_order_relaxed);
240+
241+
return true;
242+
}
243+
244+
/**
245+
* Read multichannel audio block (consumer only)
246+
* Non-blocking, returns available samples up to numSamples
247+
*
248+
* @param channelData Array of channel pointers [numChannels][numSamples]
249+
* @param numSamples Maximum samples to read per channel
250+
* @return Number of samples actually read per channel
251+
*/
252+
uint32_t read(float* const* channelData, uint32_t numSamples) {
253+
if (!initialized || !header || !audioData || numSamples == 0) {
254+
return 0;
255+
}
256+
257+
const uint64_t readIdx = header->readIndex.load(std::memory_order_relaxed);
258+
const uint64_t writeIdx = header->writeIndex.load(std::memory_order_acquire);
259+
260+
// Calculate available samples
261+
const uint64_t available = writeIdx - readIdx;
262+
const uint32_t toRead = static_cast<uint32_t>(std::min(static_cast<uint64_t>(numSamples), available));
263+
264+
if (toRead == 0) {
265+
return 0;
266+
}
267+
268+
// Read interleaved samples
269+
for (uint32_t s = 0; s < toRead; ++s) {
270+
const uint64_t ringPos = (readIdx + s) % capacitySamples;
271+
const size_t baseIdx = ringPos * numChannels;
272+
273+
for (uint32_t ch = 0; ch < numChannels; ++ch) {
274+
channelData[ch][s] = audioData[baseIdx + ch];
275+
}
276+
}
277+
278+
// Update read index
279+
header->readIndex.store(readIdx + toRead, std::memory_order_release);
280+
281+
return toRead;
282+
}
283+
284+
/**
285+
* Get number of samples available to read
286+
*/
287+
uint64_t availableToRead() const {
288+
if (!initialized || !header) return 0;
289+
const uint64_t readIdx = header->readIndex.load(std::memory_order_relaxed);
290+
const uint64_t writeIdx = header->writeIndex.load(std::memory_order_acquire);
291+
return writeIdx - readIdx;
292+
}
293+
294+
/**
295+
* Get number of samples available to write before overwriting unread data
296+
*/
297+
uint64_t availableToWrite() const {
298+
if (!initialized || !header) return 0;
299+
const uint64_t readIdx = header->readIndex.load(std::memory_order_acquire);
300+
const uint64_t writeIdx = header->writeIndex.load(std::memory_order_relaxed);
301+
return capacitySamples - (writeIdx - readIdx);
302+
}
303+
304+
/**
305+
* Calculate required memory size for given configuration
306+
*/
307+
static size_t getRequiredSize(uint32_t numChannels, uint32_t capacitySamples) {
308+
return sizeof(StreamHeader) + (numChannels * capacitySamples * sizeof(float));
309+
}
310+
311+
/**
312+
* Access header for updating metadata (producer only)
313+
*/
314+
StreamHeader* getHeader() { return header; }
315+
const StreamHeader* getHeader() const { return header; }
316+
317+
bool isInitialized() const { return initialized; }
318+
uint32_t getNumChannels() const { return numChannels; }
319+
uint32_t getCapacitySamples() const { return capacitySamples; }
320+
321+
/**
322+
* Mark stream as inactive (producer cleanup)
323+
*/
324+
void deactivate() {
325+
if (header) {
326+
header->streamActive = 0;
327+
}
328+
}
329+
330+
/**
331+
* Check if stream is active
332+
*/
333+
bool isStreamActive() const {
334+
return header && header->streamActive != 0;
335+
}
336+
337+
private:
338+
StreamHeader* header = nullptr;
339+
float* audioData = nullptr;
340+
uint32_t numChannels = 0;
341+
uint32_t capacitySamples = 0;
342+
bool initialized = false;
343+
};
344+
345+
} // namespace Mach1
346+

0 commit comments

Comments
 (0)