Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Introduced a ref-counted `ImageMetadata` object (`ImageMetadata::Ptr`) attached to `Image` and `ImageFormatReader::metadata`. DPI, text entries, and raw binary chunks are all accessed through the metadata object only when requested via `Options`.
- Lossless roundtrip tests for all formats (BMP, PNG, WebP, TGA, TIFF, PPM, GIF) now verify pixel-perfect fidelity after write→read; animated roundtrip tests for GIF, WebP, and PNG verify per-frame pixel integrity.

### Audio GUI (`yup_audio_gui`)

- `SpectrogramComponent` now keeps its waterfall history on the GPU: a precompiled `.ysl` shader bundle (embedded in `yup_SpectrogramComponentShader.inc`, built with the `yup_shader_bundler` host tool) drives a single fullscreen-triangle `GpuRenderPass` (see `GpuPipeline`) that scrolls the previous frame down by the pending rows and writes the new rows with the color map applied entirely on the GPU, uploading only the raw magnitudes as a uniform buffer - no per-paint CPU pixel upload, no GPU texture creation, and no 2D canvas flush (if the bundle cannot be compiled no waterfall is rendered). Pending FFT rows are always consumed (applied or dropped) so the update queue can never accumulate. The log-frequency → FFT-bin mapping is precomputed once per configuration instead of recomputed with pow/log per row, and the frequency grid (lines + labels) is cached in an offscreen canvas and only re-rendered when the frequency range or size changes. The component now requires a GPU render context (the CPU `Image` fallback was removed). The component's per-frame `refreshDisplay` hook processes pending FFT rows, and the history is presented at a fractional vertical offset that advances at the FFT row rate, so the waterfall scrolls smoothly between rows instead of jumping a row per update; the offset is clamped to a single row so bursts of FFT rows can never push the waterfall off-screen. The scroll speed is adjustable via the new `setScrollSpeed()` multiplier (1.0 = realtime, 0.0 = paused).

### Shading

Expand Down
166 changes: 152 additions & 14 deletions examples/graphics/source/examples/SpectrumAnalyzer.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include <yup_audio_basics/yup_audio_basics.h>

#include <array>
#include <atomic>
#include <cmath>
#include <vector>

Expand Down Expand Up @@ -376,8 +377,7 @@ class SignalGenerator

float generatePinkNoise()
{
// Paul Kellett's refined method for pink noise
float white = yup::Random::getSystemRandom().nextFloat() * 2.0f - 1.0f;
float white = generateWhiteNoise();

pinkFilters[0] = 0.99886f * pinkFilters[0] + white * 0.0555179f;
pinkFilters[1] = 0.99332f * pinkFilters[1] + white * 0.0750759f;
Expand All @@ -394,10 +394,12 @@ class SignalGenerator

float generateBrownNoise()
{
float white = yup::Random::getSystemRandom().nextFloat() * 2.0f - 1.0f;
brownState = (brownState + (0.02f * white)) / 1.02f;
brownState *= 3.5f; // Scale up
return brownState;
float white = generateWhiteNoise();

brownState = (brownState + 0.02f * white) / 1.02f;

float output = brownState * 3.5f;
return yup::jlimit (output, -1.0f, 1.0f);
}

void updatePhaseIncrement()
Expand Down Expand Up @@ -445,6 +447,99 @@ class SignalGenerator
int pendingReadPosition = 0;
};

//==============================================================================
// Plays a decoded audio file (e.g. the bundled mp3) through the demo, resampled
// to the device rate and looped. The whole file is decoded to mono floats on
// load so playback is a simple indexed read on the audio thread.
class AudioFilePlayer
{
public:
bool load (yup::AudioFormatManager& formatManager, const yup::File& file)
{
auto newReader = formatManager.createReaderFor (file);
if (newReader == nullptr)
return false;

const double newSampleRate = newReader->sampleRate > 0 ? newReader->sampleRate : 44100.0;
const int numChannels = yup::jmax (1, newReader->numChannels);
const auto numFrames = newReader->lengthInSamples;
if (numFrames <= 0)
return false;

// Decode the whole file into an AudioBuffer, then downmix to mono
// floats so playback is an indexed read.
yup::AudioBuffer<float> decoded (numChannels, static_cast<int> (numFrames));
if (! newReader->read (&decoded, 0, static_cast<int> (numFrames), 0, true, numChannels > 1))
return false;

std::vector<float> mono (static_cast<std::size_t> (numFrames), 0.0f);
const float gain = 1.0f / static_cast<float> (numChannels);
for (int ch = 0; ch < numChannels; ++ch)
{
const auto* channelData = decoded.getReadPointer (ch);
for (std::size_t i = 0; i < mono.size(); ++i)
mono[i] += channelData[i] * gain;
}

reader = std::move (newReader);
samples = std::move (mono);
sourceSampleRate = newSampleRate;
position = 0.0;
return true;
}

bool isLoaded() const noexcept { return ! samples.empty(); }

void setAmplitude (float value) { gain = value; }

void setTargetSampleRate (double newRate) noexcept
{
targetSampleRate = newRate > 0 ? newRate : 44100.0;
}

void renderNextBlock (float* output, int numSamples) noexcept
{
if (output == nullptr || numSamples <= 0)
return;

if (samples.empty())
{
for (int i = 0; i < numSamples; ++i)
output[i] = 0.0f;
return;
}

const std::size_t length = samples.size();
const double ratio = sourceSampleRate / targetSampleRate;

if (position >= static_cast<double> (length))
position = std::fmod (position, static_cast<double> (length));

const float gainToApply = gain.load();
for (int i = 0; i < numSamples; ++i)
{
// Linear interpolation between adjacent source samples, wrapping at
// the end of the file to loop playback.
const std::size_t i0 = static_cast<std::size_t> (position) % length;
const std::size_t i1 = (i0 + 1) % length;
const float s0 = samples[i0];
const float s1 = samples[i1];
const float frac = static_cast<float> (position - static_cast<double> (static_cast<std::size_t> (position)));
output[i] = (s0 + frac * (s1 - s0)) * gainToApply;

position += ratio;
}
}

private:
std::unique_ptr<yup::AudioFormatReader> reader;
std::vector<float> samples;
double sourceSampleRate = 44100.0;
double targetSampleRate = 44100.0;
double position = 0.0;
std::atomic<float> gain = 1.0f;
};

//==============================================================================

class SpectrumAnalyzerDemo
Expand All @@ -463,8 +558,9 @@ class SpectrumAnalyzerDemo
SpectrumAnalyzerDemo()
: Component ("SpectrumAnalyzerDemo")
, analyzerComponent (analyzerState)
, spectrogramComponent (analyzerState)
, spectrogramComponent (spectrogramState)
{
loadAudioFile(); // Pre-decode the bundled mp3 before the audio device starts
setupUI();
setupAudio();
}
Expand Down Expand Up @@ -560,7 +656,10 @@ class SpectrumAnalyzerDemo
if (monoOutputBuffer.size() < static_cast<std::size_t> (numSamples))
monoOutputBuffer.resize (static_cast<std::size_t> (numSamples), 0.0f);

signalGenerator.renderNextBlock (monoOutputBuffer.data(), numSamples);
if (useAudioFile.load() && filePlayer.isLoaded())
filePlayer.renderNextBlock (monoOutputBuffer.data(), numSamples);
else
signalGenerator.renderNextBlock (monoOutputBuffer.data(), numSamples);

for (int sample = 0; sample < numSamples; ++sample)
{
Expand All @@ -570,8 +669,10 @@ class SpectrumAnalyzerDemo
for (int channel = 0; channel < numOutputChannels; ++channel)
outputChannelData[channel][sample] = audioSample;

// Feed to spectrum analyzer
// Feed to both spectrum displays (each owns its own analysis state
// so they do not compete for the same FIFO and starve each other)
analyzerState.pushSample (audioSample);
spectrogramState.pushSample (audioSample);
}
}

Expand All @@ -589,10 +690,14 @@ class SpectrumAnalyzerDemo
signalGenerator.setAmplitude (currentAmplitude);
signalGenerator.setSweepParameters (20.0, 22000.0, sweepDurationSeconds);
monoOutputBuffer.assign (static_cast<std::size_t> (maxBlockSize), 0.0f);

// Match the file player's resampler to the device rate.
filePlayer.setTargetSampleRate (sampleRate);
}

// Configure spectrum analyzer
// Configure spectrum displays
analyzerComponent.setSampleRate (sampleRate);
spectrogramComponent.setSampleRate (sampleRate);
}

void audioDeviceStopped() override
Expand Down Expand Up @@ -622,6 +727,7 @@ class SpectrumAnalyzerDemo
signalTypeCombo->addItem ("White Noise", 7);
signalTypeCombo->addItem ("Pink Noise", 8);
signalTypeCombo->addItem ("Brown Noise", 9);
signalTypeCombo->addItem ("Audio File", 10);
signalTypeCombo->setSelectedId (3);
signalTypeCombo->onSelectedItemChanged = [this]
{
Expand Down Expand Up @@ -651,9 +757,10 @@ class SpectrumAnalyzerDemo
amplitudeSlider->onValueChanged = [this] (double value)
{
currentAmplitude = (float) value;
updateSignalGenerator ([value] (SignalGenerator& generator)
updateSignalGenerator ([value, this] (SignalGenerator& generator)
{
generator.setAmplitude ((float) value);
filePlayer.setAmplitude ((float) value);
});
};
addAndMakeVisible (*amplitudeSlider);
Expand Down Expand Up @@ -745,6 +852,7 @@ class SpectrumAnalyzerDemo
overlapSlider->onValueChanged = [this] (double value)
{
analyzerComponent.setOverlapFactor ((float) value);
spectrogramComponent.setOverlapFactor ((float) value);
};
addAndMakeVisible (*overlapSlider);

Expand Down Expand Up @@ -818,7 +926,6 @@ class SpectrumAnalyzerDemo
spectrogramComponent.setWindowType (yup::WindowType::hann);
spectrogramComponent.setFrequencyRange (20.0f, 22000.0f);
spectrogramComponent.setDecibelRange (-100.0f, 10.0f);
spectrogramComponent.setUpdateRate (25);
spectrogramComponent.setSampleRate (44100.0);
spectrogramComponent.setOverlapFactor (0.75f);
spectrogramComponent.setColorMap (yup::SpectrogramColorMap::Type::heatmap);
Expand Down Expand Up @@ -933,6 +1040,23 @@ class SpectrumAnalyzerDemo
fftInfoLabel->setBounds (fftStatus);
}

// Decodes the bundled mp3 into the file player so it is ready (and
// immutable) before the audio device starts rendering.
void loadAudioFile()
{
formatManager.registerDefaultFormats();

auto dataDir = yup::File (__FILE__)
.getParentDirectory()
.getParentDirectory()
.getParentDirectory()
.getChildFile ("data");

auto audioFile = dataDir.getChildFile ("break_boomblastic_92bpm.mp3");
if (audioFile.existsAsFile())
filePlayer.load (formatManager, audioFile);
}

void updateSignalType()
{
SignalGenerator::SignalType signalType = SignalGenerator::SignalType::singleTone;
Expand Down Expand Up @@ -974,6 +1098,11 @@ class SpectrumAnalyzerDemo
break;
}

// The "Audio File" source plays the pre-decoded mp3 through the file
// player instead of the signal generator (loaded in the constructor, so
// it is immutable while the audio callback reads it).
useAudioFile = (signalTypeCombo->getSelectedId() == 10);

updateSignalGenerator ([signalType, sweepPlaybackMode] (SignalGenerator& generator)
{
generator.setSignalType (signalType);
Expand All @@ -990,8 +1119,9 @@ class SpectrumAnalyzerDemo
int selectedId = fftSizeCombo->getSelectedId();
currentFFTSize = 64 << (selectedId - 1); // 64, 128, 256, ..., 16384

// Update the analyzer component (which will update the state)
// Update both displays (each owns its own state)
analyzerComponent.setFFTSize (currentFFTSize);
spectrogramComponent.setFFTSize (currentFFTSize);
}

void updateWindowType()
Expand Down Expand Up @@ -1036,6 +1166,7 @@ class SpectrumAnalyzerDemo
}

analyzerComponent.setWindowType (windowType);
spectrogramComponent.setWindowType (windowType);
}

void updateDisplayType()
Expand Down Expand Up @@ -1136,9 +1267,16 @@ class SpectrumAnalyzerDemo
yup::AudioDeviceManager deviceManager;
SignalGenerator signalGenerator;

// Spectrum analyzer
// Optional audio-file playback (the bundled mp3)
yup::AudioFormatManager formatManager;
AudioFilePlayer filePlayer;
std::atomic<bool> useAudioFile { false };

// Spectrum displays (each with its own analysis state - sharing one FIFO
// between two consumers would starve the spectrogram)
yup::SpectrumAnalyzerState analyzerState;
yup::SpectrumAnalyzerComponent analyzerComponent;
yup::SpectrumAnalyzerState spectrogramState;
yup::SpectrogramComponent spectrogramComponent;

// UI components
Expand Down
Loading
Loading