Skip to content

Commit 352ac7e

Browse files
committed
feat(oscilloscope): Implement dynamic rendering strategy for oscilloscope
1 parent bf7048a commit 352ac7e

3 files changed

Lines changed: 305 additions & 27 deletions

File tree

src/dmt/gui/widget/Oscilloscope.h

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131

3232
//==============================================================================
3333

34+
#include "gui/widget/PathStrokeRenderer.h"
3435
#include <JuceHeader.h>
36+
#include <memory>
3537

3638
//==============================================================================
3739

@@ -72,6 +74,7 @@ class alignas(64) Oscilloscope : public juce::Thread
7274
using PixelFormat = juce::Image::PixelFormat;
7375
using ReadWriteLock = juce::ReadWriteLock;
7476
using Settings = dmt::Settings;
77+
using Renderer = OscilloscopeRenderer<SampleType>;
7578

7679
//==============================================================================
7780
/**
@@ -91,6 +94,7 @@ class alignas(64) Oscilloscope : public juce::Thread
9194
, ringBuffer(_ringBuffer)
9295
, channel(_channel)
9396
, size(_sizeFactor)
97+
, renderer(std::make_unique<PathStrokeRenderer<SampleType>>())
9498
{
9599
startThread();
96100
}
@@ -187,6 +191,23 @@ class alignas(64) Oscilloscope : public juce::Thread
187191
thickness = _newThickness;
188192
}
189193

194+
//==============================================================================
195+
/**
196+
* @brief Sets the rendering strategy for the oscilloscope.
197+
*
198+
* @param _newRenderer A unique pointer to the new renderer implementation.
199+
*
200+
* @details
201+
* Swaps the current rendering strategy under the write lock to ensure
202+
* thread safety with the rendering thread. The previous renderer is
203+
* destroyed when the new one is set.
204+
*/
205+
inline void setRenderer(std::unique_ptr<Renderer> _newRenderer)
206+
{
207+
const ScopedWriteLock writeLock(imageLock);
208+
renderer = std::move(_newRenderer);
209+
}
210+
190211
//==============================================================================
191212
protected:
192213
//==============================================================================
@@ -282,32 +303,18 @@ class alignas(64) Oscilloscope : public juce::Thread
282303
width - pixelToDraw + 10, 0, pixelToDraw, height);
283304
image.clear(clearRect, juce::Colours::transparentBlack);
284305

285-
// Generate path for new samples
286-
currentX = currentX - static_cast<int>(currentX) + width - pixelToDraw;
287-
float pixelsPerSample = 1.0f / samplesPerPixel;
288-
289-
const float startY = halfHeight + currentSample * halfHeight * amplitude;
290-
const auto startPoint = juce::Point<float>(currentX, startY);
291-
292-
juce::Path path;
293-
path.startNewSubPath(startPoint);
294-
295-
for (size_t i = 0; i < static_cast<size_t>(samplesToDraw); ++i) {
296-
const int sampleIndex = firstSamplesToDraw + static_cast<int>(i);
297-
currentSample = ringBuffer.getSample(channel, sampleIndex);
298-
currentX += pixelsPerSample;
299-
const float y = halfHeight + currentSample * halfHeight * amplitude;
300-
const auto point = juce::Point<float>(currentX, y);
301-
path.lineTo(point);
302-
}
303-
304-
juce::PathStrokeType strokeType(thickness * size,
305-
juce::PathStrokeType::JointStyle::mitered,
306-
juce::PathStrokeType::EndCapStyle::square);
307-
306+
// Delegate drawing to the active renderer
308307
juce::Graphics imageGraphics(image);
309-
imageGraphics.setColour(juce::Colours::white);
310-
imageGraphics.strokePath(path, strokeType);
308+
const typename Renderer::RenderContext context{ firstSamplesToDraw,
309+
samplesToDraw,
310+
static_cast<float>(
311+
width - pixelToDraw),
312+
1.0f / samplesPerPixel,
313+
halfHeight,
314+
amplitude,
315+
thickness,
316+
size };
317+
renderer->draw(imageGraphics, ringBuffer, channel, context);
311318
}
312319

313320
//==============================================================================
@@ -323,8 +330,7 @@ class alignas(64) Oscilloscope : public juce::Thread
323330
Image image = Image(PixelFormat::ARGB, 1, 1, true);
324331
ReadWriteLock imageLock;
325332

326-
SampleType currentSample = static_cast<SampleType>(0.0f);
327-
float currentX = 0.0f;
333+
std::unique_ptr<Renderer> renderer;
328334
float rawSamplesPerPixel = 10.0f;
329335
float amplitude = 1.0f;
330336
float thickness = 3.0f;
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
//==============================================================================
2+
/*
3+
* ██████ ██ ███ ███ ███████ ████████ ██ ██ ██████ ██ ██ ██ ██
4+
* ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
5+
* ██ ██ ██ ██ ████ ██ █████ ██ ███████ ██ ██ ███ ████
6+
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
7+
* ██████ ██ ██ ██ ███████ ██ ██ ██ ██████ ██ ██ ██
8+
*
9+
* Copyright (C) 2024 Dimethoxy Audio (https://dimethoxy.com)
10+
*
11+
* This file is part of the Dimethoxy Library, a collection of essential
12+
* classes used across various Dimethoxy projects.
13+
* These files are primarily designed for internal use within our repositories.
14+
*
15+
* License:
16+
* This code is licensed under the GPLv3 license. You are permitted to use and
17+
* modify this code under the terms of this license.
18+
* You must adhere GPLv3 license for any project using this code or parts of it.
19+
* Your are not allowed to use this code in any closed-source project.
20+
*
21+
* Description:
22+
* Abstract base class for oscilloscope rendering strategies. Defines the
23+
* interface for drawing waveform data onto a JUCE Graphics context. Concrete
24+
* implementations provide different visualization algorithms while the
25+
* Oscilloscope class handles image management, scrolling, and threading.
26+
*
27+
* Authors:
28+
* Lunix-420 (Primary Author)
29+
*/
30+
//==============================================================================
31+
32+
#pragma once
33+
34+
//==============================================================================
35+
36+
#include <JuceHeader.h>
37+
38+
//==============================================================================
39+
40+
namespace dmt {
41+
namespace gui {
42+
namespace widget {
43+
44+
//==============================================================================
45+
/**
46+
* @brief Abstract base class for oscilloscope rendering strategies.
47+
*
48+
* @tparam SampleType The sample type (e.g., float, double) used for audio data.
49+
*
50+
* @details
51+
* This class defines the interface for rendering waveform data onto a JUCE
52+
* Graphics context. Concrete implementations provide different visualization
53+
* algorithms (e.g., path stroking, point plotting, filled waveforms).
54+
*
55+
* The renderer reads sample data directly from the ring buffer by reference,
56+
* avoiding any additional allocations or data copies. The Oscilloscope class
57+
* handles all image management (scrolling, clearing) and passes a
58+
* pre-configured Graphics context along with a RenderContext struct containing
59+
* the parameters needed for drawing.
60+
*
61+
* Renderers may maintain their own persistent state between frames (e.g.,
62+
* sub-pixel position tracking) to ensure visual continuity.
63+
*/
64+
template<typename SampleType>
65+
class OscilloscopeRenderer
66+
{
67+
//============================================================================
68+
public:
69+
using RingBuffer = dmt::dsp::data::RingAudioBuffer<SampleType>;
70+
71+
//============================================================================
72+
/**
73+
* @brief Parameters passed from the Oscilloscope to the renderer each frame.
74+
*
75+
* @details
76+
* This struct contains all pre-computed values the renderer needs to draw
77+
* the new waveform segment. The Oscilloscope computes these from its own
78+
* state and the ring buffer, then passes them to the renderer. This avoids
79+
* the renderer needing to know about image scrolling or buffer management.
80+
*/
81+
struct RenderContext
82+
{
83+
/** Index of the first sample to read from the ring buffer. */
84+
int firstSampleIndex;
85+
86+
/** Number of samples to draw in this frame. */
87+
int sampleCount;
88+
89+
/** X coordinate where the new drawing region starts on the image. */
90+
float drawStartX;
91+
92+
/** Horizontal spacing: pixels per sample. */
93+
float pixelsPerSample;
94+
95+
/** Vertical center of the drawing area in pixels. */
96+
int halfHeight;
97+
98+
/** Amplitude scaling factor for the waveform. */
99+
float amplitude;
100+
101+
/** Stroke thickness for the waveform. */
102+
float thickness;
103+
104+
/** Global size scaling factor. */
105+
float sizeFactor;
106+
};
107+
108+
//============================================================================
109+
/**
110+
* @brief Virtual destructor for safe polymorphic deletion.
111+
*/
112+
virtual ~OscilloscopeRenderer() = default;
113+
114+
//============================================================================
115+
/**
116+
* @brief Draws a waveform segment onto the provided Graphics context.
117+
*
118+
* @param _graphics The JUCE Graphics context targeting the oscilloscope
119+
* image. Already configured for the current frame.
120+
* @param _ringBuffer Reference to the ring buffer containing audio samples.
121+
* Read directly — no data is copied.
122+
* @param _channel The audio channel index to read from.
123+
* @param _context Pre-computed rendering parameters for this frame.
124+
*
125+
* @details
126+
* Implementations should read samples directly from the ring buffer using
127+
* the indices provided in the context. The Graphics context is already
128+
* attached to the oscilloscope image, and the image has already been
129+
* scrolled and cleared by the Oscilloscope class.
130+
*/
131+
virtual void draw(juce::Graphics& _graphics,
132+
RingBuffer& _ringBuffer,
133+
int _channel,
134+
const RenderContext& _context) = 0;
135+
};
136+
137+
} // namespace widget
138+
} // namespace gui
139+
} // namespace dmt
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
//==============================================================================
2+
/*
3+
* ██████ ██ ███ ███ ███████ ████████ ██ ██ ██████ ██ ██ ██ ██
4+
* ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
5+
* ██ ██ ██ ██ ████ ██ █████ ██ ███████ ██ ██ ███ ████
6+
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
7+
* ██████ ██ ██ ██ ███████ ██ ██ ██ ██████ ██ ██ ██
8+
*
9+
* Copyright (C) 2024 Dimethoxy Audio (https://dimethoxy.com)
10+
*
11+
* This file is part of the Dimethoxy Library, a collection of essential
12+
* classes used across various Dimethoxy projects.
13+
* These files are primarily designed for internal use within our repositories.
14+
*
15+
* License:
16+
* This code is licensed under the GPLv3 license. You are permitted to use and
17+
* modify this code under the terms of this license.
18+
* You must adhere GPLv3 license for any project using this code or parts of it.
19+
* Your are not allowed to use this code in any closed-source project.
20+
*
21+
* Description:
22+
* Path stroke oscilloscope renderer. Draws a continuous waveform path using
23+
* JUCE's Path and PathStrokeType for high-quality, anti-aliased rendering.
24+
* This is the original (and most expensive) rendering algorithm.
25+
*
26+
* Authors:
27+
* Lunix-420 (Primary Author)
28+
*/
29+
//==============================================================================
30+
31+
#pragma once
32+
33+
//==============================================================================
34+
35+
#include "gui/widget/OscilloscopeRenderer.h"
36+
#include <JuceHeader.h>
37+
38+
//==============================================================================
39+
40+
namespace dmt {
41+
namespace gui {
42+
namespace widget {
43+
44+
//==============================================================================
45+
/**
46+
* @brief Path stroke oscilloscope renderer for high-quality waveform drawing.
47+
*
48+
* @tparam SampleType The sample type (e.g., float, double) used for audio data.
49+
*
50+
* @details
51+
* This renderer builds a continuous JUCE Path from the audio samples and
52+
* strokes it with a configurable thickness. It produces high-quality,
53+
* anti-aliased waveform visuals at the cost of higher CPU usage compared to
54+
* simpler rendering strategies.
55+
*
56+
* The renderer maintains persistent state (currentX and currentSample) between
57+
* frames to ensure visual continuity of the waveform across render calls.
58+
* Sub-pixel positioning is preserved to avoid visual jitter.
59+
*
60+
* Samples are read directly from the ring buffer — no data is copied.
61+
*/
62+
template<typename SampleType>
63+
class PathStrokeRenderer : public OscilloscopeRenderer<SampleType>
64+
{
65+
//============================================================================
66+
public:
67+
using RingBuffer = typename OscilloscopeRenderer<SampleType>::RingBuffer;
68+
using RenderContext =
69+
typename OscilloscopeRenderer<SampleType>::RenderContext;
70+
71+
//============================================================================
72+
/**
73+
* @brief Draws a waveform segment using path stroking.
74+
*
75+
* @param _graphics The JUCE Graphics context targeting the oscilloscope
76+
* image.
77+
* @param _ringBuffer Reference to the ring buffer containing audio samples.
78+
* @param _channel The audio channel index to read from.
79+
* @param _context Pre-computed rendering parameters for this frame.
80+
*
81+
* @details
82+
* Builds a continuous path from the provided samples, maintaining sub-pixel
83+
* continuity with the previous frame via the persistent currentX state.
84+
* The path is stroked with mitered joints and square end caps for a clean
85+
* waveform appearance.
86+
*/
87+
inline void draw(juce::Graphics& _graphics,
88+
RingBuffer& _ringBuffer,
89+
int _channel,
90+
const RenderContext& _context) override
91+
{
92+
// Maintain sub-pixel continuity across frames
93+
currentX = currentX - static_cast<int>(currentX) + _context.drawStartX;
94+
95+
const float startY =
96+
static_cast<float>(_context.halfHeight) +
97+
currentSample * _context.halfHeight * _context.amplitude;
98+
const auto startPoint = juce::Point<float>(currentX, startY);
99+
100+
juce::Path path;
101+
path.startNewSubPath(startPoint);
102+
103+
for (size_t i = 0; i < static_cast<size_t>(_context.sampleCount); ++i) {
104+
const int sampleIndex = _context.firstSampleIndex + static_cast<int>(i);
105+
currentSample = _ringBuffer.getSample(_channel, sampleIndex);
106+
currentX += _context.pixelsPerSample;
107+
const float y = static_cast<float>(_context.halfHeight) +
108+
currentSample * _context.halfHeight * _context.amplitude;
109+
const auto point = juce::Point<float>(currentX, y);
110+
path.lineTo(point);
111+
}
112+
113+
juce::PathStrokeType strokeType(_context.thickness * _context.sizeFactor,
114+
juce::PathStrokeType::JointStyle::mitered,
115+
juce::PathStrokeType::EndCapStyle::square);
116+
117+
_graphics.setColour(juce::Colours::white);
118+
_graphics.strokePath(path, strokeType);
119+
}
120+
121+
//============================================================================
122+
private:
123+
//============================================================================
124+
/** Last sample value for waveform continuity between frames. */
125+
SampleType currentSample = static_cast<SampleType>(0.0f);
126+
127+
/** Current X position with sub-pixel precision for visual continuity. */
128+
float currentX = 0.0f;
129+
};
130+
131+
} // namespace widget
132+
} // namespace gui
133+
} // namespace dmt

0 commit comments

Comments
 (0)