mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-15 15:50:59 +02:00
spectrogram and waveform
This commit is contained in:
@@ -34,6 +34,7 @@ add_library(prism_tui_analysis STATIC
|
||||
src/dashboard_layout.cpp
|
||||
src/display_model.cpp
|
||||
src/meter_display_model.cpp
|
||||
src/scrolling_history.cpp
|
||||
src/scope_plot_model.cpp
|
||||
src/spectrum_peak_model.cpp
|
||||
src/tui_settings.cpp
|
||||
@@ -43,6 +44,8 @@ add_library(prism_tui_analysis STATIC
|
||||
${PRISM_NATIVE_DIR}/multiband.cpp
|
||||
${PRISM_NATIVE_DIR}/vumeter.cpp
|
||||
${PRISM_NATIVE_DIR}/lufsmeter.cpp
|
||||
${PRISM_NATIVE_DIR}/spectrogram.cpp
|
||||
${PRISM_NATIVE_DIR}/waveform.cpp
|
||||
${PRISM_NATIVE_DIR}/dsp_utils.cpp)
|
||||
target_include_directories(prism_tui_analysis PUBLIC
|
||||
src
|
||||
|
||||
@@ -25,7 +25,13 @@ int normalizedOscilloscopeDisplaySamples(float sampleRate) {
|
||||
} // namespace
|
||||
|
||||
AnalysisPipeline::AnalysisPipeline(float sampleRate, size_t fftSize)
|
||||
: spectrum_(fftSize), sampleRate_(sampleRate), fftSize_(fftSize) {
|
||||
: spectrum_(fftSize),
|
||||
spectrogramDisplayHistory_(kSpectrogramHistoryRows, kSpectrogramHistoryColumns),
|
||||
spectrogramHeatHistory_(kSpectrogramHistoryRows, kSpectrogramHistoryColumns),
|
||||
waveformHistory_(Visualizer::WAVEFORM_STEREO_SUMMARY_STRIDE,
|
||||
kWaveformHistoryColumns),
|
||||
sampleRate_(sampleRate),
|
||||
fftSize_(fftSize) {
|
||||
spectrum_.setSampleRate(sampleRate);
|
||||
spectrum_.setSmoothing(0.9f);
|
||||
vu_.setSampleRate(sampleRate);
|
||||
@@ -34,6 +40,8 @@ AnalysisPipeline::AnalysisPipeline(float sampleRate, size_t fftSize)
|
||||
oscilloscope_.setPitchLock(true);
|
||||
oscilloscope_.setDisplaySamples(normalizedOscilloscopeDisplaySamples(sampleRate));
|
||||
vectorscope_.setSampleRate(sampleRate);
|
||||
setSpectrogramSettings(2.0f, 1.0f, 4.0f, "sharper", "log", "horizontal");
|
||||
setWaveformSettings(false, 1);
|
||||
}
|
||||
|
||||
void AnalysisPipeline::process(const Prism::Capture::AudioChunk& chunk) {
|
||||
@@ -62,6 +70,30 @@ void AnalysisPipeline::process(const Prism::Capture::AudioChunk& chunk) {
|
||||
}
|
||||
oscilloscope_.pushSamples(monoScratch_.data(), count);
|
||||
vectorscope_.pushMultibandSamples(left, right, count);
|
||||
|
||||
const auto spectrogramColumns = spectrogram_.process(monoScratch_.data(), count);
|
||||
spectrogramDisplayHistory_.append(spectrogramColumns.display);
|
||||
spectrogramHeatHistory_.append(spectrogramColumns.heat);
|
||||
|
||||
if (waveformStereo_) {
|
||||
const auto& columns = waveform_.processStereo(left, right, count);
|
||||
waveformHistory_.append(columns);
|
||||
} else {
|
||||
const auto& columns = waveform_.processMono(monoScratch_.data(), count);
|
||||
const size_t columnCount = columns.size() / Visualizer::WAVEFORM_MONO_SUMMARY_STRIDE;
|
||||
waveformMonoScratch_.resize(
|
||||
columnCount * Visualizer::WAVEFORM_STEREO_SUMMARY_STRIDE);
|
||||
for (size_t column = 0; column < columnCount; ++column) {
|
||||
const size_t source = column * Visualizer::WAVEFORM_MONO_SUMMARY_STRIDE;
|
||||
const size_t destination = column * Visualizer::WAVEFORM_STEREO_SUMMARY_STRIDE;
|
||||
for (size_t value = 0; value < Visualizer::WAVEFORM_MONO_SUMMARY_STRIDE; ++value) {
|
||||
waveformMonoScratch_[destination + value] = columns[source + value];
|
||||
waveformMonoScratch_[destination + Visualizer::WAVEFORM_MONO_SUMMARY_STRIDE + value] =
|
||||
columns[source + value];
|
||||
}
|
||||
}
|
||||
waveformHistory_.append(waveformMonoScratch_);
|
||||
}
|
||||
}
|
||||
|
||||
AnalysisFrame AnalysisPipeline::snapshot() {
|
||||
@@ -102,6 +134,10 @@ AnalysisFrame AnalysisPipeline::snapshot() {
|
||||
kVectorscopeDisplayPoints);
|
||||
frame.vectorscope.multibandPoints.resize(
|
||||
frame.vectorscope.pointCount * Visualizer::MULTIBAND_POINT_STRIDE);
|
||||
frame.spectrogram.display = spectrogramDisplayHistory_.snapshot();
|
||||
frame.spectrogram.heat = spectrogramHeatHistory_.snapshot();
|
||||
frame.waveform.history = waveformHistory_.snapshot();
|
||||
frame.waveform.stereo = waveformStereo_;
|
||||
return frame;
|
||||
}
|
||||
|
||||
@@ -111,10 +147,16 @@ void AnalysisPipeline::reset() {
|
||||
lufs_.reset();
|
||||
oscilloscope_.reset();
|
||||
vectorscope_.reset();
|
||||
spectrogram_.reset();
|
||||
waveform_.reset();
|
||||
spectrogramDisplayHistory_.reset();
|
||||
spectrogramHeatHistory_.reset();
|
||||
waveformHistory_.reset();
|
||||
spectrumPeakTracker_.reset();
|
||||
monoScratch_.clear();
|
||||
trimmedLeftScratch_.clear();
|
||||
trimmedRightScratch_.clear();
|
||||
waveformMonoScratch_.clear();
|
||||
displayPitch_ = 0.0f;
|
||||
}
|
||||
|
||||
@@ -133,6 +175,43 @@ void AnalysisPipeline::setOscilloscopePitchLock(bool enabled) {
|
||||
oscilloscope_.setPitchLock(enabled);
|
||||
}
|
||||
|
||||
void AnalysisPipeline::setSpectrogramSettings(float scrollSpeed,
|
||||
float contrast,
|
||||
float tiltDbPerOctave,
|
||||
const std::string& clarityMode,
|
||||
const std::string& scaleMode,
|
||||
const std::string& orientation) {
|
||||
Visualizer::SpectrogramConfig config;
|
||||
config.fftSize = fftSize_;
|
||||
config.sampleRate = sampleRate_;
|
||||
config.rowCount = kSpectrogramHistoryRows;
|
||||
config.minFrequency = 20.0f;
|
||||
config.maxFrequency = 20000.0f;
|
||||
config.minDecibels = -90.0f;
|
||||
config.maxDecibels = -12.0f;
|
||||
config.scrollSpeed = std::clamp(scrollSpeed, 0.5f, 4.0f);
|
||||
config.contrast = std::clamp(contrast, 0.5f, 2.0f);
|
||||
config.tiltDbPerOctave = std::clamp(tiltDbPerOctave, -2.0f, 8.0f);
|
||||
config.clarityMode = clarityMode;
|
||||
config.scaleMode = scaleMode;
|
||||
config.orientation = orientation;
|
||||
spectrogram_.configure(config);
|
||||
spectrogramDisplayHistory_.reset();
|
||||
spectrogramHeatHistory_.reset();
|
||||
}
|
||||
|
||||
void AnalysisPipeline::setWaveformSettings(bool stereo, int scrollSpeed) {
|
||||
waveformStereo_ = stereo;
|
||||
waveformScrollSpeed_ = std::clamp(scrollSpeed, 1, 8);
|
||||
constexpr float baseColumnsPerSecond = 128.0f;
|
||||
const size_t samplesPerColumn = static_cast<size_t>(std::max(
|
||||
1.0f,
|
||||
std::round(sampleRate_ /
|
||||
(baseColumnsPerSecond * static_cast<float>(waveformScrollSpeed_)))));
|
||||
waveform_.configure(sampleRate_, samplesPerColumn);
|
||||
waveformHistory_.reset();
|
||||
}
|
||||
|
||||
size_t drainCapture(Prism::Capture::SystemAudioCapture& capture,
|
||||
AnalysisPipeline& pipeline,
|
||||
bool& captureOverrun,
|
||||
|
||||
@@ -2,19 +2,26 @@
|
||||
|
||||
#include "lufsmeter.h"
|
||||
#include "oscilloscope.h"
|
||||
#include "scrolling_history.h"
|
||||
#include "spectrogram.h"
|
||||
#include "spectrum.h"
|
||||
#include "spectrum_peak_model.h"
|
||||
#include "system_audio_capture.h"
|
||||
#include "vectorscope.h"
|
||||
#include "vumeter.h"
|
||||
#include "waveform.h"
|
||||
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Prism::Tui {
|
||||
|
||||
constexpr size_t kDefaultFftSize = 4096;
|
||||
constexpr size_t kVectorscopeDisplayPoints = 4096;
|
||||
constexpr size_t kSpectrogramHistoryRows = 128;
|
||||
constexpr size_t kSpectrogramHistoryColumns = 1024;
|
||||
constexpr size_t kWaveformHistoryColumns = 2048;
|
||||
|
||||
struct OscilloscopeFrame {
|
||||
std::vector<float> samples;
|
||||
@@ -27,6 +34,16 @@ struct VectorscopeFrame {
|
||||
size_t pointCount = 0;
|
||||
};
|
||||
|
||||
struct SpectrogramFrame {
|
||||
ScrollingHistoryFrame display;
|
||||
ScrollingHistoryFrame heat;
|
||||
};
|
||||
|
||||
struct WaveformFrame {
|
||||
ScrollingHistoryFrame history;
|
||||
bool stereo = false;
|
||||
};
|
||||
|
||||
struct AnalysisFrame {
|
||||
std::vector<float> magnitudes;
|
||||
std::optional<SpectrumPeakInfo> spectrumPeak;
|
||||
@@ -34,6 +51,8 @@ struct AnalysisFrame {
|
||||
Visualizer::LUFSMeterSnapshot lufs{};
|
||||
OscilloscopeFrame oscilloscope;
|
||||
VectorscopeFrame vectorscope;
|
||||
SpectrogramFrame spectrogram;
|
||||
WaveformFrame waveform;
|
||||
};
|
||||
|
||||
class AnalysisPipeline {
|
||||
@@ -46,6 +65,13 @@ public:
|
||||
void setInputTrimDb(float db);
|
||||
void setSpectrumTilt(float dbPerOctave);
|
||||
void setOscilloscopePitchLock(bool enabled);
|
||||
void setSpectrogramSettings(float scrollSpeed,
|
||||
float contrast,
|
||||
float tiltDbPerOctave,
|
||||
const std::string& clarityMode,
|
||||
const std::string& scaleMode,
|
||||
const std::string& orientation);
|
||||
void setWaveformSettings(bool stereo, int scrollSpeed);
|
||||
|
||||
private:
|
||||
Visualizer::Spectrum spectrum_;
|
||||
@@ -53,15 +79,23 @@ private:
|
||||
Visualizer::LUFSMeterAnalyzer lufs_;
|
||||
Visualizer::Oscilloscope oscilloscope_;
|
||||
Visualizer::Vectorscope vectorscope_;
|
||||
Visualizer::SpectrogramAnalyzer spectrogram_;
|
||||
Visualizer::WaveformMultibandAnalyzer waveform_;
|
||||
ScrollingHistory spectrogramDisplayHistory_;
|
||||
ScrollingHistory spectrogramHeatHistory_;
|
||||
ScrollingHistory waveformHistory_;
|
||||
SpectrumPeakTracker spectrumPeakTracker_;
|
||||
std::vector<float> monoScratch_;
|
||||
std::vector<float> trimmedLeftScratch_;
|
||||
std::vector<float> trimmedRightScratch_;
|
||||
std::vector<float> waveformMonoScratch_;
|
||||
float sampleRate_ = 48000.0f;
|
||||
size_t fftSize_ = kDefaultFftSize;
|
||||
float inputGainLinear_ = 1.0f;
|
||||
float spectrumTiltDbPerOctave_ = 2.0f;
|
||||
float displayPitch_ = 0.0f;
|
||||
bool waveformStereo_ = false;
|
||||
int waveformScrollSpeed_ = 1;
|
||||
};
|
||||
|
||||
size_t drainCapture(Prism::Capture::SystemAudioCapture& capture,
|
||||
|
||||
+2
-1
@@ -60,10 +60,11 @@ std::string usageText() {
|
||||
"Controls:\n"
|
||||
" Tab / Shift-Tab Focus the next or previous panel.\n"
|
||||
" Enter Expand the focused panel or restore the dashboard.\n"
|
||||
" s Open settings for the focused scope.\n"
|
||||
" s Open settings.\n"
|
||||
" l Cycle automatic, stacked, and column layouts.\n"
|
||||
" v Cycle vectorscope display modes.\n"
|
||||
" 1 / 2 / 3 / 4 / 5 Focus Spectrum, Oscilloscope, Vectorscope, VU, or LUFS.\n"
|
||||
" 6 / 7 Focus Spectrogram or Waveform.\n"
|
||||
" r Reset analyzers and integrated loudness.\n"
|
||||
" q / Esc / Ctrl-C Quit.\n";
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ MinimumSize panelMinimumSize(PanelId panel) {
|
||||
return {30, 5};
|
||||
case PanelId::LUFSMeter:
|
||||
return {30, 7};
|
||||
case PanelId::Spectrogram:
|
||||
return {30, 8};
|
||||
case PanelId::Waveform:
|
||||
return {30, 7};
|
||||
}
|
||||
return {1, 1};
|
||||
}
|
||||
@@ -155,6 +159,21 @@ LayoutPreset resolvePreset(LayoutPreset requested, int width, int height) {
|
||||
|
||||
LayoutNode makeRoot(LayoutPreset preset, int width, int height) {
|
||||
if (preset == LayoutPreset::Columns) {
|
||||
if (width >= 108 && height >= 34) {
|
||||
return LayoutNode::split(SplitAxis::Columns, {
|
||||
LayoutNode::split(SplitAxis::Rows, {
|
||||
LayoutNode::leaf(PanelId::Spectrum, 3),
|
||||
LayoutNode::leaf(PanelId::Oscilloscope, 2),
|
||||
LayoutNode::leaf(PanelId::Waveform, 2),
|
||||
}, 3),
|
||||
LayoutNode::split(SplitAxis::Rows, {
|
||||
LayoutNode::leaf(PanelId::Vectorscope, 2),
|
||||
LayoutNode::leaf(PanelId::VUMeter, 1),
|
||||
LayoutNode::leaf(PanelId::LUFSMeter, 1),
|
||||
LayoutNode::leaf(PanelId::Spectrogram, 2),
|
||||
}, 1),
|
||||
});
|
||||
}
|
||||
return LayoutNode::split(SplitAxis::Columns, {
|
||||
LayoutNode::split(SplitAxis::Rows, {
|
||||
LayoutNode::leaf(PanelId::Spectrum, 3),
|
||||
@@ -253,6 +272,8 @@ std::vector<PanelId> panelOrder() {
|
||||
PanelId::Vectorscope,
|
||||
PanelId::VUMeter,
|
||||
PanelId::LUFSMeter,
|
||||
PanelId::Spectrogram,
|
||||
PanelId::Waveform,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ enum class PanelId {
|
||||
Vectorscope,
|
||||
VUMeter,
|
||||
LUFSMeter,
|
||||
Spectrogram,
|
||||
Waveform,
|
||||
};
|
||||
|
||||
enum class SplitAxis {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#include "scrolling_history.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace Prism::Tui {
|
||||
|
||||
ScrollingHistory::ScrollingHistory(size_t columnStride, size_t capacityColumns)
|
||||
: columnStride_(columnStride),
|
||||
capacityColumns_(capacityColumns),
|
||||
values_(columnStride * capacityColumns, 0.0f) {
|
||||
if (columnStride == 0 || capacityColumns == 0) {
|
||||
throw std::invalid_argument("scrolling history dimensions must be non-zero");
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollingHistory::append(const float* values, size_t columnCount) {
|
||||
if (values == nullptr || columnCount == 0) {
|
||||
return;
|
||||
}
|
||||
if (columnCount >= capacityColumns_) {
|
||||
values += (columnCount - capacityColumns_) * columnStride_;
|
||||
columnCount = capacityColumns_;
|
||||
std::copy(values,
|
||||
values + columnCount * columnStride_,
|
||||
values_.begin());
|
||||
columnCount_ = capacityColumns_;
|
||||
writeColumn_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t column = 0; column < columnCount; ++column) {
|
||||
const size_t destination = writeColumn_ * columnStride_;
|
||||
const size_t source = column * columnStride_;
|
||||
std::copy(values + source,
|
||||
values + source + columnStride_,
|
||||
values_.begin() + static_cast<std::ptrdiff_t>(destination));
|
||||
writeColumn_ = (writeColumn_ + 1) % capacityColumns_;
|
||||
columnCount_ = std::min(columnCount_ + 1, capacityColumns_);
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollingHistory::append(const std::vector<float>& values) {
|
||||
append(values.data(), values.size() / columnStride_);
|
||||
}
|
||||
|
||||
ScrollingHistoryFrame ScrollingHistory::snapshot() const {
|
||||
ScrollingHistoryFrame frame;
|
||||
frame.columnCount = columnCount_;
|
||||
frame.columnStride = columnStride_;
|
||||
frame.values.resize(columnCount_ * columnStride_);
|
||||
if (columnCount_ == 0) {
|
||||
return frame;
|
||||
}
|
||||
|
||||
const size_t oldest = columnCount_ == capacityColumns_ ? writeColumn_ : 0;
|
||||
for (size_t column = 0; column < columnCount_; ++column) {
|
||||
const size_t sourceColumn = (oldest + column) % capacityColumns_;
|
||||
std::copy(values_.begin() + static_cast<std::ptrdiff_t>(sourceColumn * columnStride_),
|
||||
values_.begin() + static_cast<std::ptrdiff_t>((sourceColumn + 1) * columnStride_),
|
||||
frame.values.begin() + static_cast<std::ptrdiff_t>(column * columnStride_));
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
void ScrollingHistory::reset() {
|
||||
columnCount_ = 0;
|
||||
writeColumn_ = 0;
|
||||
}
|
||||
|
||||
} // namespace Prism::Tui
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace Prism::Tui {
|
||||
|
||||
struct ScrollingHistoryFrame {
|
||||
std::vector<float> values;
|
||||
size_t columnCount = 0;
|
||||
size_t columnStride = 0;
|
||||
};
|
||||
|
||||
// Fixed-capacity column storage. Audio processing appends into the ring while
|
||||
// snapshots expose a stable, chronological copy to the renderer.
|
||||
class ScrollingHistory {
|
||||
public:
|
||||
ScrollingHistory(size_t columnStride, size_t capacityColumns);
|
||||
|
||||
void append(const float* values, size_t columnCount);
|
||||
void append(const std::vector<float>& values);
|
||||
ScrollingHistoryFrame snapshot() const;
|
||||
void reset();
|
||||
|
||||
size_t columnCount() const { return columnCount_; }
|
||||
size_t columnStride() const { return columnStride_; }
|
||||
|
||||
private:
|
||||
size_t columnStride_ = 0;
|
||||
size_t capacityColumns_ = 0;
|
||||
size_t columnCount_ = 0;
|
||||
size_t writeColumn_ = 0;
|
||||
std::vector<float> values_;
|
||||
};
|
||||
|
||||
} // namespace Prism::Tui
|
||||
+330
-3
@@ -47,6 +47,48 @@ std::chrono::microseconds displayFrameInterval(int framesPerSecond) {
|
||||
1000000 / std::max(1, framesPerSecond));
|
||||
}
|
||||
|
||||
const char* spectrogramClarityConfig(SpectrogramClarity clarity) {
|
||||
switch (clarity) {
|
||||
case SpectrogramClarity::Classic: return "classic";
|
||||
case SpectrogramClarity::Sharp: return "sharp";
|
||||
case SpectrogramClarity::Sharper: return "sharper";
|
||||
}
|
||||
return "sharper";
|
||||
}
|
||||
|
||||
const char* spectrogramScaleConfig(SpectrogramScale scale) {
|
||||
switch (scale) {
|
||||
case SpectrogramScale::Mel: return "mel";
|
||||
case SpectrogramScale::Logarithmic: return "log";
|
||||
case SpectrogramScale::Linear: return "linear";
|
||||
}
|
||||
return "log";
|
||||
}
|
||||
|
||||
const char* spectrogramOrientationConfig(SpectrogramOrientation orientation) {
|
||||
return orientation == SpectrogramOrientation::Vertical
|
||||
? "vertical"
|
||||
: "horizontal";
|
||||
}
|
||||
|
||||
void applySpectrogramSettings(AnalysisPipeline& pipeline,
|
||||
const TuiSettings& settings) {
|
||||
pipeline.setSpectrogramSettings(
|
||||
settings.spectrogramScrollSpeed,
|
||||
settings.spectrogramContrast,
|
||||
settings.spectrogramTiltDbPerOctave,
|
||||
spectrogramClarityConfig(settings.spectrogramClarity),
|
||||
spectrogramScaleConfig(settings.spectrogramScale),
|
||||
spectrogramOrientationConfig(settings.spectrogramOrientation));
|
||||
}
|
||||
|
||||
void applyWaveformSettings(AnalysisPipeline& pipeline,
|
||||
const TuiSettings& settings) {
|
||||
pipeline.setWaveformSettings(
|
||||
settings.waveformMode == WaveformMode::Stereo,
|
||||
settings.waveformScrollSpeed);
|
||||
}
|
||||
|
||||
volatile std::sig_atomic_t signalRequested = 0;
|
||||
|
||||
void handleSignal(int) {
|
||||
@@ -77,6 +119,8 @@ struct DisplayFrame {
|
||||
Visualizer::LUFSMeterSnapshot lufs{};
|
||||
OscilloscopeFrame oscilloscope;
|
||||
VectorscopeFrame vectorscope;
|
||||
SpectrogramFrame spectrogram;
|
||||
WaveformFrame waveform;
|
||||
double sampleRate = 48000.0;
|
||||
std::string backend;
|
||||
std::string device;
|
||||
@@ -90,7 +134,7 @@ struct InterfaceState {
|
||||
bool settingsOpen = false;
|
||||
SettingsPage settingsPage = SettingsPage::Home;
|
||||
size_t settingsHomeSelection = 0;
|
||||
std::array<size_t, 7> settingsSelections{};
|
||||
std::array<size_t, 9> settingsSelections{};
|
||||
std::string settingsStatus;
|
||||
};
|
||||
|
||||
@@ -157,6 +201,10 @@ std::string panelName(PanelId panel) {
|
||||
return "VU Meter";
|
||||
case PanelId::LUFSMeter:
|
||||
return "LUFS Meter";
|
||||
case PanelId::Spectrogram:
|
||||
return "Spectrogram";
|
||||
case PanelId::Waveform:
|
||||
return "Waveform";
|
||||
}
|
||||
return "Panel";
|
||||
}
|
||||
@@ -173,6 +221,10 @@ std::string panelNumber(PanelId panel) {
|
||||
return "4";
|
||||
case PanelId::LUFSMeter:
|
||||
return "5";
|
||||
case PanelId::Spectrogram:
|
||||
return "6";
|
||||
case PanelId::Waveform:
|
||||
return "7";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
@@ -907,6 +959,247 @@ ftxui::Element renderLUFSMeterPanel(const DisplayFrame& frame,
|
||||
size(HEIGHT, EQUAL, std::max(1, height));
|
||||
}
|
||||
|
||||
ftxui::Color interpolateColor(const std::array<int, 3>& from,
|
||||
const std::array<int, 3>& to,
|
||||
float amount) {
|
||||
const float t = std::clamp(amount, 0.0f, 1.0f);
|
||||
return ftxui::Color::RGB(
|
||||
static_cast<uint8_t>(std::lround(from[0] + (to[0] - from[0]) * t)),
|
||||
static_cast<uint8_t>(std::lround(from[1] + (to[1] - from[1]) * t)),
|
||||
static_cast<uint8_t>(std::lround(from[2] + (to[2] - from[2]) * t)));
|
||||
}
|
||||
|
||||
ftxui::Color spectrogramColor(float intensity, SpectrogramColorMode mode) {
|
||||
const float value = std::clamp(intensity, 0.0f, 1.0f);
|
||||
if (mode == SpectrogramColorMode::Mono) {
|
||||
return interpolateColor(
|
||||
{{5, 12, 14}}, {{102, 255, 255}}, std::pow(value, 0.72f));
|
||||
}
|
||||
|
||||
constexpr std::array<float, 6> stops = {0.0f, 0.16f, 0.36f, 0.58f, 0.78f, 1.0f};
|
||||
constexpr std::array<std::array<int, 3>, 6> colors = {{
|
||||
{{5, 3, 12}},
|
||||
{{15, 7, 33}},
|
||||
{{61, 11, 94}},
|
||||
{{163, 26, 121}},
|
||||
{{255, 82, 87}},
|
||||
{{255, 241, 209}},
|
||||
}};
|
||||
size_t upper = 1;
|
||||
while (upper + 1 < stops.size() && value > stops[upper]) ++upper;
|
||||
const size_t lower = upper - 1;
|
||||
const float span = stops[upper] - stops[lower];
|
||||
return interpolateColor(
|
||||
colors[lower], colors[upper],
|
||||
span > 0.0f ? (value - stops[lower]) / span : 0.0f);
|
||||
}
|
||||
|
||||
float maxHistoryValue(const ScrollingHistoryFrame& history,
|
||||
size_t column,
|
||||
size_t firstRow,
|
||||
size_t lastRow) {
|
||||
if (column >= history.columnCount || history.columnStride == 0 ||
|
||||
history.values.size() < history.columnCount * history.columnStride) {
|
||||
return 0.0f;
|
||||
}
|
||||
firstRow = std::min(firstRow, history.columnStride - 1);
|
||||
lastRow = std::min(std::max(firstRow + 1, lastRow), history.columnStride);
|
||||
float result = 0.0f;
|
||||
const size_t offset = column * history.columnStride;
|
||||
for (size_t row = firstRow; row < lastRow; ++row) {
|
||||
result = std::max(result, history.values[offset + row]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ftxui::Element renderSpectrogramPanel(const DisplayFrame& frame,
|
||||
int width,
|
||||
int height,
|
||||
bool focused,
|
||||
const TuiSettings& settings) {
|
||||
using namespace ftxui;
|
||||
const bool heat = settings.spectrogramColor == SpectrogramColorMode::Heat;
|
||||
const auto& history = heat ? frame.spectrogram.heat : frame.spectrogram.display;
|
||||
const bool vertical =
|
||||
settings.spectrogramOrientation == SpectrogramOrientation::Vertical;
|
||||
auto plot = canvas([
|
||||
history,
|
||||
colorMode = settings.spectrogramColor,
|
||||
vertical
|
||||
](Canvas& surface) {
|
||||
const int cellColumns = surface.width() / 2;
|
||||
const int cellRows = surface.height() / 4;
|
||||
if (cellColumns <= 0 || cellRows <= 0 || history.columnCount == 0 ||
|
||||
history.columnStride == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int timeCells = vertical ? cellRows : cellColumns;
|
||||
const int frequencyCells = vertical ? cellColumns : cellRows;
|
||||
const size_t visibleColumns = std::min(
|
||||
history.columnCount, static_cast<size_t>(timeCells));
|
||||
const size_t sourceStart = history.columnCount - visibleColumns;
|
||||
const int destinationStart = timeCells - static_cast<int>(visibleColumns);
|
||||
for (size_t time = 0; time < visibleColumns; ++time) {
|
||||
const size_t sourceColumn = sourceStart + time;
|
||||
const int timeCell = destinationStart + static_cast<int>(time);
|
||||
for (int frequencyCell = 0; frequencyCell < frequencyCells; ++frequencyCell) {
|
||||
const float normalizedStart = static_cast<float>(frequencyCell) /
|
||||
static_cast<float>(frequencyCells);
|
||||
const float normalizedEnd = static_cast<float>(frequencyCell + 1) /
|
||||
static_cast<float>(frequencyCells);
|
||||
const size_t firstRow = static_cast<size_t>(std::floor(
|
||||
normalizedStart * static_cast<float>(history.columnStride)));
|
||||
const size_t lastRow = static_cast<size_t>(std::ceil(
|
||||
normalizedEnd * static_cast<float>(history.columnStride)));
|
||||
const float intensity = maxHistoryValue(
|
||||
history, sourceColumn, firstRow, lastRow);
|
||||
if (!std::isfinite(intensity) || intensity < 0.008f) continue;
|
||||
|
||||
const int cellX = vertical ? frequencyCell : timeCell;
|
||||
const int cellY = vertical
|
||||
? timeCell
|
||||
: frequencyCell;
|
||||
const Color color = spectrogramColor(intensity, colorMode);
|
||||
for (int dx = 0; dx < 2; ++dx) {
|
||||
surface.DrawBlock(cellX * 2 + dx, cellY * 4, true, color);
|
||||
surface.DrawBlock(cellX * 2 + dx, cellY * 4 + 2, true, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}) | flex;
|
||||
|
||||
std::string detail = std::string(spectrogramColorName(settings.spectrogramColor));
|
||||
if (width >= 48) {
|
||||
detail += " • " + std::string(spectrogramScaleName(settings.spectrogramScale));
|
||||
}
|
||||
if (width >= 66) {
|
||||
detail += " • " + std::string(
|
||||
spectrogramOrientationName(settings.spectrogramOrientation));
|
||||
}
|
||||
auto panel = window(
|
||||
panelTitle(PanelId::Spectrogram, focused, detail),
|
||||
std::move(plot));
|
||||
return stylePanel(std::move(panel), focused) |
|
||||
size(WIDTH, EQUAL, std::max(1, width)) |
|
||||
size(HEIGHT, EQUAL, std::max(1, height));
|
||||
}
|
||||
|
||||
ftxui::Color waveformBandColor(const float* summary, bool multiband) {
|
||||
using namespace ftxui;
|
||||
if (!multiband || summary == nullptr) return Color::CyanLight;
|
||||
std::array<float, 3> weights = {
|
||||
std::max(0.0f, summary[2]),
|
||||
std::max(0.0f, summary[3]),
|
||||
std::max(0.0f, summary[4]),
|
||||
};
|
||||
float total = weights[0] + weights[1] + weights[2];
|
||||
if (total <= 1.0e-8f) return Color::CyanLight;
|
||||
for (float& weight : weights) {
|
||||
weight = std::pow(weight / total, 2.6f);
|
||||
}
|
||||
total = weights[0] + weights[1] + weights[2];
|
||||
for (float& weight : weights) {
|
||||
weight /= std::max(total, 1.0e-8f);
|
||||
}
|
||||
constexpr std::array<std::array<int, 3>, 3> colors = {{
|
||||
{{255, 68, 68}},
|
||||
{{68, 221, 68}},
|
||||
{{68, 136, 255}},
|
||||
}};
|
||||
std::array<int, 3> mixed{};
|
||||
for (size_t channel = 0; channel < mixed.size(); ++channel) {
|
||||
for (size_t band = 0; band < weights.size(); ++band) {
|
||||
mixed[channel] += static_cast<int>(std::lround(
|
||||
weights[band] * static_cast<float>(colors[band][channel])));
|
||||
}
|
||||
}
|
||||
return Color::RGB(
|
||||
std::clamp(mixed[0], 0, 255),
|
||||
std::clamp(mixed[1], 0, 255),
|
||||
std::clamp(mixed[2], 0, 255));
|
||||
}
|
||||
|
||||
ftxui::Element renderWaveformPanel(const DisplayFrame& frame,
|
||||
int width,
|
||||
int height,
|
||||
bool focused,
|
||||
const TuiSettings& settings) {
|
||||
using namespace ftxui;
|
||||
const bool stereo = settings.waveformMode == WaveformMode::Stereo;
|
||||
auto plot = canvas([
|
||||
history = frame.waveform.history,
|
||||
stereo,
|
||||
multiband = settings.waveformMultiband
|
||||
](Canvas& surface) {
|
||||
const int canvasWidth = surface.width();
|
||||
const int canvasHeight = surface.height();
|
||||
if (canvasWidth <= 0 || canvasHeight <= 0) return;
|
||||
|
||||
const int laneCount = stereo ? 2 : 1;
|
||||
for (int lane = 0; lane < laneCount; ++lane) {
|
||||
const int centerY = static_cast<int>(std::lround(
|
||||
(static_cast<float>(lane) + 0.5f) *
|
||||
static_cast<float>(canvasHeight) / static_cast<float>(laneCount)));
|
||||
surface.DrawPointLine(
|
||||
0, centerY, canvasWidth - 1, centerY,
|
||||
Color::RGB(55, 61, 64));
|
||||
}
|
||||
if (stereo) {
|
||||
surface.DrawPointLine(
|
||||
0, canvasHeight / 2, canvasWidth - 1, canvasHeight / 2,
|
||||
Color::RGB(40, 45, 48));
|
||||
}
|
||||
|
||||
if (history.columnCount == 0 ||
|
||||
history.columnStride < Visualizer::WAVEFORM_STEREO_SUMMARY_STRIDE ||
|
||||
history.values.size() < history.columnCount * history.columnStride) {
|
||||
return;
|
||||
}
|
||||
const size_t visibleColumns = std::min(
|
||||
history.columnCount, static_cast<size_t>(canvasWidth));
|
||||
const size_t sourceStart = history.columnCount - visibleColumns;
|
||||
const int destinationStart = canvasWidth - static_cast<int>(visibleColumns);
|
||||
const float laneHeight = static_cast<float>(canvasHeight) /
|
||||
static_cast<float>(laneCount);
|
||||
const float radius = std::max(1.0f, laneHeight * 0.44f);
|
||||
for (size_t column = 0; column < visibleColumns; ++column) {
|
||||
const size_t sourceColumn = sourceStart + column;
|
||||
const float* summary = history.values.data() +
|
||||
sourceColumn * history.columnStride;
|
||||
const int x = destinationStart + static_cast<int>(column);
|
||||
for (int lane = 0; lane < laneCount; ++lane) {
|
||||
const float* channel = summary +
|
||||
(lane == 0 ? 0 : Visualizer::WAVEFORM_MONO_SUMMARY_STRIDE);
|
||||
const float minimum = std::clamp(channel[0], -1.0f, 1.0f);
|
||||
const float maximum = std::clamp(channel[1], -1.0f, 1.0f);
|
||||
const float centerY = (static_cast<float>(lane) + 0.5f) * laneHeight;
|
||||
const int top = std::clamp(
|
||||
static_cast<int>(std::lround(centerY - maximum * radius)),
|
||||
0, canvasHeight - 1);
|
||||
const int bottom = std::clamp(
|
||||
static_cast<int>(std::lround(centerY - minimum * radius)),
|
||||
0, canvasHeight - 1);
|
||||
surface.DrawBlockLine(
|
||||
x, std::min(top, bottom), x, std::max(top, bottom),
|
||||
waveformBandColor(channel, multiband));
|
||||
}
|
||||
}
|
||||
}) | flex;
|
||||
|
||||
std::string detail = waveformModeName(settings.waveformMode);
|
||||
if (settings.waveformMultiband) detail += " • Multiband";
|
||||
if (width >= 58) {
|
||||
detail += " • " + std::to_string(settings.waveformScrollSpeed) + "×";
|
||||
}
|
||||
auto panel = window(
|
||||
panelTitle(PanelId::Waveform, focused, detail),
|
||||
std::move(plot));
|
||||
return stylePanel(std::move(panel), focused) |
|
||||
size(WIDTH, EQUAL, std::max(1, width)) |
|
||||
size(HEIGHT, EQUAL, std::max(1, height));
|
||||
}
|
||||
|
||||
const PanelRect* findPanelRect(const DashboardLayout& layout, PanelId panel) {
|
||||
const auto found = std::find_if(
|
||||
layout.panels.begin(), layout.panels.end(),
|
||||
@@ -953,6 +1246,20 @@ ftxui::Element renderLayoutNode(const LayoutNode& node,
|
||||
rect->height,
|
||||
focused,
|
||||
state.settings);
|
||||
case PanelId::Spectrogram:
|
||||
return renderSpectrogramPanel(
|
||||
frame,
|
||||
rect->width,
|
||||
rect->height,
|
||||
focused,
|
||||
state.settings);
|
||||
case PanelId::Waveform:
|
||||
return renderWaveformPanel(
|
||||
frame,
|
||||
rect->width,
|
||||
rect->height,
|
||||
focused,
|
||||
state.settings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1181,6 +1488,8 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
||||
pipeline.setInputTrimDb(appliedSettings.inputTrimDb);
|
||||
pipeline.setSpectrumTilt(appliedSettings.spectrumTiltDbPerOctave);
|
||||
pipeline.setOscilloscopePitchLock(appliedSettings.oscilloscopePitchLock);
|
||||
applySpectrogramSettings(pipeline, appliedSettings);
|
||||
applyWaveformSettings(pipeline, appliedSettings);
|
||||
|
||||
bool captureOverrun = false;
|
||||
auto nextFrameAt = std::chrono::steady_clock::now();
|
||||
@@ -1204,6 +1513,20 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
||||
requestedSettings.spectrumTiltDbPerOctave);
|
||||
pipeline.setOscilloscopePitchLock(
|
||||
requestedSettings.oscilloscopePitchLock);
|
||||
const bool spectrogramAnalysisChanged =
|
||||
requestedSettings.spectrogramClarity != appliedSettings.spectrogramClarity ||
|
||||
requestedSettings.spectrogramScale != appliedSettings.spectrogramScale ||
|
||||
requestedSettings.spectrogramOrientation != appliedSettings.spectrogramOrientation ||
|
||||
requestedSettings.spectrogramScrollSpeed != appliedSettings.spectrogramScrollSpeed ||
|
||||
requestedSettings.spectrogramContrast != appliedSettings.spectrogramContrast ||
|
||||
requestedSettings.spectrogramTiltDbPerOctave != appliedSettings.spectrogramTiltDbPerOctave;
|
||||
if (spectrogramAnalysisChanged) {
|
||||
applySpectrogramSettings(pipeline, requestedSettings);
|
||||
}
|
||||
if (requestedSettings.waveformMode != appliedSettings.waveformMode ||
|
||||
requestedSettings.waveformScrollSpeed != appliedSettings.waveformScrollSpeed) {
|
||||
applyWaveformSettings(pipeline, requestedSettings);
|
||||
}
|
||||
appliedSettings = requestedSettings;
|
||||
if (refreshChanged) {
|
||||
nextFrameAt = std::chrono::steady_clock::now();
|
||||
@@ -1222,6 +1545,8 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
||||
next.lufs = analyzed.lufs;
|
||||
next.oscilloscope = std::move(analyzed.oscilloscope);
|
||||
next.vectorscope = std::move(analyzed.vectorscope);
|
||||
next.spectrogram = std::move(analyzed.spectrogram);
|
||||
next.waveform = std::move(analyzed.waveform);
|
||||
next.sampleRate = started.sampleRate;
|
||||
next.backend = capture->backendName();
|
||||
next.device = started.deviceLabel.empty() ? started.deviceId : started.deviceLabel;
|
||||
@@ -1230,8 +1555,8 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
||||
if (running.load() && !redrawQueued.exchange(true)) {
|
||||
screen.PostEvent(Event::Custom);
|
||||
}
|
||||
nextFrameAt = now + displayFrameInterval(
|
||||
appliedSettings.refreshRate);
|
||||
nextFrameAt = now +
|
||||
displayFrameInterval(appliedSettings.refreshRate);
|
||||
}
|
||||
std::this_thread::sleep_for(kCapturePollInterval);
|
||||
}
|
||||
@@ -1427,6 +1752,8 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
||||
if (event == Event::Character('3')) selectedPanel = PanelId::Vectorscope;
|
||||
if (event == Event::Character('4')) selectedPanel = PanelId::VUMeter;
|
||||
if (event == Event::Character('5')) selectedPanel = PanelId::LUFSMeter;
|
||||
if (event == Event::Character('6')) selectedPanel = PanelId::Spectrogram;
|
||||
if (event == Event::Character('7')) selectedPanel = PanelId::Waveform;
|
||||
if (selectedPanel) {
|
||||
interfaceState.focusedPanel = *selectedPanel;
|
||||
if (interfaceState.expandedPanel) {
|
||||
|
||||
+238
-3
@@ -45,6 +45,22 @@ const std::vector<SettingDescriptor> kLUFSMeterSettings = {
|
||||
{SettingId::LUFSReadout, "Loudness readout", "Chooses which LUFS window drives the main bar and badge."},
|
||||
};
|
||||
|
||||
const std::vector<SettingDescriptor> kSpectrogramSettings = {
|
||||
{SettingId::SpectrogramColor, "Color", "Switches between Prism's heat palette and a focused mono display."},
|
||||
{SettingId::SpectrogramClarity, "Clarity", "Controls frequency reassignment and spectral sharpness."},
|
||||
{SettingId::SpectrogramScale, "Frequency scale", "Chooses logarithmic, mel, or linear frequency spacing."},
|
||||
{SettingId::SpectrogramOrientation, "Orientation", "Scrolls time horizontally or vertically."},
|
||||
{SettingId::SpectrogramScrollSpeed, "Scroll speed", "Changes how quickly new time columns move through the pane."},
|
||||
{SettingId::SpectrogramContrast, "Contrast", "Shapes the intensity range without changing the analyzer input."},
|
||||
{SettingId::SpectrogramTilt, "Display tilt", "Offsets frequencies by decibels per octave around 1 kHz."},
|
||||
};
|
||||
|
||||
const std::vector<SettingDescriptor> kWaveformSettings = {
|
||||
{SettingId::WaveformMode, "Channels", "Shows one mono envelope or separate left and right lanes."},
|
||||
{SettingId::WaveformScrollSpeed, "Scroll speed", "Changes the time resolution of the rolling waveform."},
|
||||
{SettingId::WaveformMultiband, "Multiband color", "Colors each envelope column by its dominant frequency band."},
|
||||
};
|
||||
|
||||
float snap(float value, float step) {
|
||||
return std::round(value / step) * step;
|
||||
}
|
||||
@@ -104,6 +120,38 @@ std::string serializeLufsReadout(LUFSReadout readout) {
|
||||
return "short_term";
|
||||
}
|
||||
|
||||
std::string serializeSpectrogramColor(SpectrogramColorMode mode) {
|
||||
return mode == SpectrogramColorMode::Mono ? "mono" : "heat";
|
||||
}
|
||||
|
||||
std::string serializeSpectrogramClarity(SpectrogramClarity clarity) {
|
||||
switch (clarity) {
|
||||
case SpectrogramClarity::Classic: return "classic";
|
||||
case SpectrogramClarity::Sharp: return "sharp";
|
||||
case SpectrogramClarity::Sharper: return "sharper";
|
||||
}
|
||||
return "sharper";
|
||||
}
|
||||
|
||||
std::string serializeSpectrogramScale(SpectrogramScale scale) {
|
||||
switch (scale) {
|
||||
case SpectrogramScale::Mel: return "mel";
|
||||
case SpectrogramScale::Logarithmic: return "log";
|
||||
case SpectrogramScale::Linear: return "linear";
|
||||
}
|
||||
return "log";
|
||||
}
|
||||
|
||||
std::string serializeSpectrogramOrientation(SpectrogramOrientation orientation) {
|
||||
return orientation == SpectrogramOrientation::Vertical
|
||||
? "vertical"
|
||||
: "horizontal";
|
||||
}
|
||||
|
||||
std::string serializeWaveformMode(WaveformMode mode) {
|
||||
return mode == WaveformMode::Stereo ? "stereo" : "mono";
|
||||
}
|
||||
|
||||
bool parseBool(const std::string& value, bool fallback) {
|
||||
if (value == "true" || value == "1" || value == "on") return true;
|
||||
if (value == "false" || value == "0" || value == "off") return false;
|
||||
@@ -181,6 +229,42 @@ LUFSReadout parseLufsReadout(const std::string& value, LUFSReadout fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
SpectrogramColorMode parseSpectrogramColor(
|
||||
const std::string& value, SpectrogramColorMode fallback) {
|
||||
if (value == "heat") return SpectrogramColorMode::Heat;
|
||||
if (value == "mono") return SpectrogramColorMode::Mono;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
SpectrogramClarity parseSpectrogramClarity(
|
||||
const std::string& value, SpectrogramClarity fallback) {
|
||||
if (value == "classic") return SpectrogramClarity::Classic;
|
||||
if (value == "sharp") return SpectrogramClarity::Sharp;
|
||||
if (value == "sharper") return SpectrogramClarity::Sharper;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
SpectrogramScale parseSpectrogramScale(
|
||||
const std::string& value, SpectrogramScale fallback) {
|
||||
if (value == "mel") return SpectrogramScale::Mel;
|
||||
if (value == "log") return SpectrogramScale::Logarithmic;
|
||||
if (value == "linear") return SpectrogramScale::Linear;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
SpectrogramOrientation parseSpectrogramOrientation(
|
||||
const std::string& value, SpectrogramOrientation fallback) {
|
||||
if (value == "horizontal") return SpectrogramOrientation::Horizontal;
|
||||
if (value == "vertical") return SpectrogramOrientation::Vertical;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
WaveformMode parseWaveformMode(const std::string& value, WaveformMode fallback) {
|
||||
if (value == "mono") return WaveformMode::Mono;
|
||||
if (value == "stereo") return WaveformMode::Stereo;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const char* environmentValue(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
return value != nullptr && value[0] != '\0' ? value : nullptr;
|
||||
@@ -188,6 +272,38 @@ const char* environmentValue(const char* name) {
|
||||
|
||||
} // namespace
|
||||
|
||||
const char* spectrogramColorName(SpectrogramColorMode mode) {
|
||||
return mode == SpectrogramColorMode::Mono ? "Mono" : "Heat";
|
||||
}
|
||||
|
||||
const char* spectrogramClarityName(SpectrogramClarity clarity) {
|
||||
switch (clarity) {
|
||||
case SpectrogramClarity::Classic: return "Classic";
|
||||
case SpectrogramClarity::Sharp: return "Sharp";
|
||||
case SpectrogramClarity::Sharper: return "Sharper";
|
||||
}
|
||||
return "Sharper";
|
||||
}
|
||||
|
||||
const char* spectrogramScaleName(SpectrogramScale scale) {
|
||||
switch (scale) {
|
||||
case SpectrogramScale::Mel: return "Mel";
|
||||
case SpectrogramScale::Logarithmic: return "Log";
|
||||
case SpectrogramScale::Linear: return "Linear";
|
||||
}
|
||||
return "Log";
|
||||
}
|
||||
|
||||
const char* spectrogramOrientationName(SpectrogramOrientation orientation) {
|
||||
return orientation == SpectrogramOrientation::Vertical
|
||||
? "Vertical"
|
||||
: "Horizontal";
|
||||
}
|
||||
|
||||
const char* waveformModeName(WaveformMode mode) {
|
||||
return mode == WaveformMode::Stereo ? "Stereo" : "Mono";
|
||||
}
|
||||
|
||||
TuiSettings normalizeSettings(TuiSettings settings) {
|
||||
settings.inputTrimDb = std::clamp(snap(settings.inputTrimDb, 0.5f), -12.0f, 12.0f);
|
||||
settings.refreshRate = settings.refreshRate <= 30 ? 30 : 60;
|
||||
@@ -196,6 +312,13 @@ TuiSettings normalizeSettings(TuiSettings settings) {
|
||||
settings.oscilloscopeTraceWeight = std::clamp(settings.oscilloscopeTraceWeight, 1, 3);
|
||||
settings.vuReferenceDbfs = std::clamp(
|
||||
snap(settings.vuReferenceDbfs, 1.0f), -30.0f, 0.0f);
|
||||
settings.spectrogramScrollSpeed = std::clamp(
|
||||
snap(settings.spectrogramScrollSpeed, 0.5f), 0.5f, 4.0f);
|
||||
settings.spectrogramContrast = std::clamp(
|
||||
snap(settings.spectrogramContrast, 0.1f), 0.5f, 2.0f);
|
||||
settings.spectrogramTiltDbPerOctave = std::clamp(
|
||||
snap(settings.spectrogramTiltDbPerOctave, 0.5f), -2.0f, 8.0f);
|
||||
settings.waveformScrollSpeed = std::clamp(settings.waveformScrollSpeed, 1, 8);
|
||||
if (!settings.oscilloscopePitchLock) {
|
||||
settings.oscilloscopeFrequencyReadout = false;
|
||||
}
|
||||
@@ -218,7 +341,17 @@ bool operator==(const TuiSettings& left, const TuiSettings& right) {
|
||||
left.vuMeterOrientation == right.vuMeterOrientation &&
|
||||
left.vuNeedleChannels == right.vuNeedleChannels &&
|
||||
left.vuReferenceDbfs == right.vuReferenceDbfs &&
|
||||
left.lufsReadout == right.lufsReadout;
|
||||
left.lufsReadout == right.lufsReadout &&
|
||||
left.spectrogramColor == right.spectrogramColor &&
|
||||
left.spectrogramClarity == right.spectrogramClarity &&
|
||||
left.spectrogramScale == right.spectrogramScale &&
|
||||
left.spectrogramOrientation == right.spectrogramOrientation &&
|
||||
left.spectrogramScrollSpeed == right.spectrogramScrollSpeed &&
|
||||
left.spectrogramContrast == right.spectrogramContrast &&
|
||||
left.spectrogramTiltDbPerOctave == right.spectrogramTiltDbPerOctave &&
|
||||
left.waveformMode == right.waveformMode &&
|
||||
left.waveformScrollSpeed == right.waveformScrollSpeed &&
|
||||
left.waveformMultiband == right.waveformMultiband;
|
||||
}
|
||||
|
||||
bool operator!=(const TuiSettings& left, const TuiSettings& right) {
|
||||
@@ -233,6 +366,8 @@ std::vector<SettingsPage> settingsPages() {
|
||||
SettingsPage::Vectorscope,
|
||||
SettingsPage::VUMeter,
|
||||
SettingsPage::LUFSMeter,
|
||||
SettingsPage::Spectrogram,
|
||||
SettingsPage::Waveform,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -245,6 +380,8 @@ const char* settingsPageName(SettingsPage page) {
|
||||
case SettingsPage::Vectorscope: return "Vectorscope";
|
||||
case SettingsPage::VUMeter: return "VU meter";
|
||||
case SettingsPage::LUFSMeter: return "LUFS meter";
|
||||
case SettingsPage::Spectrogram: return "Spectrogram";
|
||||
case SettingsPage::Waveform: return "Waveform";
|
||||
}
|
||||
return "Settings";
|
||||
}
|
||||
@@ -258,6 +395,8 @@ const char* settingsPageDescription(SettingsPage page) {
|
||||
case SettingsPage::Vectorscope: return "Stereo projection and point rendering.";
|
||||
case SettingsPage::VUMeter: return "Classic level, peak, and phase metering.";
|
||||
case SettingsPage::LUFSMeter: return "Loudness window and target presentation.";
|
||||
case SettingsPage::Spectrogram: return "Frequency history, color, and time direction.";
|
||||
case SettingsPage::Waveform: return "Rolling amplitude envelope and band color.";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -270,6 +409,8 @@ const std::vector<SettingDescriptor>& settingsForPage(SettingsPage page) {
|
||||
case SettingsPage::Vectorscope: return kVectorscopeSettings;
|
||||
case SettingsPage::VUMeter: return kVUMeterSettings;
|
||||
case SettingsPage::LUFSMeter: return kLUFSMeterSettings;
|
||||
case SettingsPage::Spectrogram: return kSpectrogramSettings;
|
||||
case SettingsPage::Waveform: return kWaveformSettings;
|
||||
case SettingsPage::Home: break;
|
||||
}
|
||||
static const std::vector<SettingDescriptor> empty;
|
||||
@@ -315,6 +456,26 @@ std::string settingValue(const TuiSettings& settings, SettingId setting) {
|
||||
return trimFloat(settings.vuReferenceDbfs, 0) + " dBFS";
|
||||
case SettingId::LUFSReadout:
|
||||
return lufsReadoutName(settings.lufsReadout);
|
||||
case SettingId::SpectrogramColor:
|
||||
return spectrogramColorName(settings.spectrogramColor);
|
||||
case SettingId::SpectrogramClarity:
|
||||
return spectrogramClarityName(settings.spectrogramClarity);
|
||||
case SettingId::SpectrogramScale:
|
||||
return spectrogramScaleName(settings.spectrogramScale);
|
||||
case SettingId::SpectrogramOrientation:
|
||||
return spectrogramOrientationName(settings.spectrogramOrientation);
|
||||
case SettingId::SpectrogramScrollSpeed:
|
||||
return trimFloat(settings.spectrogramScrollSpeed, 1) + "×";
|
||||
case SettingId::SpectrogramContrast:
|
||||
return trimFloat(settings.spectrogramContrast, 1) + "×";
|
||||
case SettingId::SpectrogramTilt:
|
||||
return trimFloat(settings.spectrogramTiltDbPerOctave, 1) + " dB/oct";
|
||||
case SettingId::WaveformMode:
|
||||
return waveformModeName(settings.waveformMode);
|
||||
case SettingId::WaveformScrollSpeed:
|
||||
return std::to_string(settings.waveformScrollSpeed) + "×";
|
||||
case SettingId::WaveformMultiband:
|
||||
return boolValue(settings.waveformMultiband);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -323,7 +484,8 @@ bool settingIsBoolean(SettingId setting) {
|
||||
return setting == SettingId::SpectrumPeakReadout ||
|
||||
setting == SettingId::OscilloscopePitchLock ||
|
||||
setting == SettingId::OscilloscopeFrequencyReadout ||
|
||||
setting == SettingId::VectorscopeGuides;
|
||||
setting == SettingId::VectorscopeGuides ||
|
||||
setting == SettingId::WaveformMultiband;
|
||||
}
|
||||
|
||||
bool adjustSetting(TuiSettings& settings, SettingId setting, int direction) {
|
||||
@@ -412,6 +574,49 @@ bool adjustSetting(TuiSettings& settings, SettingId setting, int direction) {
|
||||
settings.lufsReadout = static_cast<LUFSReadout>(value);
|
||||
break;
|
||||
}
|
||||
case SettingId::SpectrogramColor:
|
||||
settings.spectrogramColor = settings.spectrogramColor == SpectrogramColorMode::Heat
|
||||
? SpectrogramColorMode::Mono
|
||||
: SpectrogramColorMode::Heat;
|
||||
break;
|
||||
case SettingId::SpectrogramClarity: {
|
||||
int value = static_cast<int>(settings.spectrogramClarity);
|
||||
value = (value + (direction > 0 ? 1 : 2)) % 3;
|
||||
settings.spectrogramClarity = static_cast<SpectrogramClarity>(value);
|
||||
break;
|
||||
}
|
||||
case SettingId::SpectrogramScale: {
|
||||
int value = static_cast<int>(settings.spectrogramScale);
|
||||
value = (value + (direction > 0 ? 1 : 2)) % 3;
|
||||
settings.spectrogramScale = static_cast<SpectrogramScale>(value);
|
||||
break;
|
||||
}
|
||||
case SettingId::SpectrogramOrientation:
|
||||
settings.spectrogramOrientation =
|
||||
settings.spectrogramOrientation == SpectrogramOrientation::Horizontal
|
||||
? SpectrogramOrientation::Vertical
|
||||
: SpectrogramOrientation::Horizontal;
|
||||
break;
|
||||
case SettingId::SpectrogramScrollSpeed:
|
||||
settings.spectrogramScrollSpeed += direction > 0 ? 0.5f : -0.5f;
|
||||
break;
|
||||
case SettingId::SpectrogramContrast:
|
||||
settings.spectrogramContrast += direction > 0 ? 0.1f : -0.1f;
|
||||
break;
|
||||
case SettingId::SpectrogramTilt:
|
||||
settings.spectrogramTiltDbPerOctave += direction > 0 ? 0.5f : -0.5f;
|
||||
break;
|
||||
case SettingId::WaveformMode:
|
||||
settings.waveformMode = settings.waveformMode == WaveformMode::Mono
|
||||
? WaveformMode::Stereo
|
||||
: WaveformMode::Mono;
|
||||
break;
|
||||
case SettingId::WaveformScrollSpeed:
|
||||
settings.waveformScrollSpeed += direction > 0 ? 1 : -1;
|
||||
break;
|
||||
case SettingId::WaveformMultiband:
|
||||
settings.waveformMultiband = !settings.waveformMultiband;
|
||||
break;
|
||||
}
|
||||
settings = normalizeSettings(settings);
|
||||
return settings != before;
|
||||
@@ -437,6 +642,16 @@ bool resetSetting(TuiSettings& settings, SettingId setting) {
|
||||
case SettingId::VUNeedleChannels: settings.vuNeedleChannels = defaults.vuNeedleChannels; break;
|
||||
case SettingId::VUReferenceLevel: settings.vuReferenceDbfs = defaults.vuReferenceDbfs; break;
|
||||
case SettingId::LUFSReadout: settings.lufsReadout = defaults.lufsReadout; break;
|
||||
case SettingId::SpectrogramColor: settings.spectrogramColor = defaults.spectrogramColor; break;
|
||||
case SettingId::SpectrogramClarity: settings.spectrogramClarity = defaults.spectrogramClarity; break;
|
||||
case SettingId::SpectrogramScale: settings.spectrogramScale = defaults.spectrogramScale; break;
|
||||
case SettingId::SpectrogramOrientation: settings.spectrogramOrientation = defaults.spectrogramOrientation; break;
|
||||
case SettingId::SpectrogramScrollSpeed: settings.spectrogramScrollSpeed = defaults.spectrogramScrollSpeed; break;
|
||||
case SettingId::SpectrogramContrast: settings.spectrogramContrast = defaults.spectrogramContrast; break;
|
||||
case SettingId::SpectrogramTilt: settings.spectrogramTiltDbPerOctave = defaults.spectrogramTiltDbPerOctave; break;
|
||||
case SettingId::WaveformMode: settings.waveformMode = defaults.waveformMode; break;
|
||||
case SettingId::WaveformScrollSpeed: settings.waveformScrollSpeed = defaults.waveformScrollSpeed; break;
|
||||
case SettingId::WaveformMultiband: settings.waveformMultiband = defaults.waveformMultiband; break;
|
||||
}
|
||||
return settings != before;
|
||||
}
|
||||
@@ -487,6 +702,16 @@ TuiSettings loadSettings(const std::filesystem::path& path) {
|
||||
else if (key == "vu_needle_channels") settings.vuNeedleChannels = parseVuNeedleChannels(value, settings.vuNeedleChannels);
|
||||
else if (key == "vu_reference_dbfs") settings.vuReferenceDbfs = parseFloat(value, settings.vuReferenceDbfs);
|
||||
else if (key == "lufs_readout") settings.lufsReadout = parseLufsReadout(value, settings.lufsReadout);
|
||||
else if (key == "spectrogram_color") settings.spectrogramColor = parseSpectrogramColor(value, settings.spectrogramColor);
|
||||
else if (key == "spectrogram_clarity") settings.spectrogramClarity = parseSpectrogramClarity(value, settings.spectrogramClarity);
|
||||
else if (key == "spectrogram_scale") settings.spectrogramScale = parseSpectrogramScale(value, settings.spectrogramScale);
|
||||
else if (key == "spectrogram_orientation") settings.spectrogramOrientation = parseSpectrogramOrientation(value, settings.spectrogramOrientation);
|
||||
else if (key == "spectrogram_scroll") settings.spectrogramScrollSpeed = parseFloat(value, settings.spectrogramScrollSpeed);
|
||||
else if (key == "spectrogram_contrast") settings.spectrogramContrast = parseFloat(value, settings.spectrogramContrast);
|
||||
else if (key == "spectrogram_tilt") settings.spectrogramTiltDbPerOctave = parseFloat(value, settings.spectrogramTiltDbPerOctave);
|
||||
else if (key == "waveform_mode") settings.waveformMode = parseWaveformMode(value, settings.waveformMode);
|
||||
else if (key == "waveform_scroll") settings.waveformScrollSpeed = parseInt(value, settings.waveformScrollSpeed);
|
||||
else if (key == "waveform_multiband") settings.waveformMultiband = parseBool(value, settings.waveformMultiband);
|
||||
}
|
||||
return normalizeSettings(settings);
|
||||
}
|
||||
@@ -523,7 +748,17 @@ bool saveSettings(const TuiSettings& rawSettings,
|
||||
<< "vu_orientation=" << serializeVuOrientation(settings.vuMeterOrientation) << '\n'
|
||||
<< "vu_needle_channels=" << serializeVuNeedleChannels(settings.vuNeedleChannels) << '\n'
|
||||
<< "vu_reference_dbfs=" << settings.vuReferenceDbfs << '\n'
|
||||
<< "lufs_readout=" << serializeLufsReadout(settings.lufsReadout) << '\n';
|
||||
<< "lufs_readout=" << serializeLufsReadout(settings.lufsReadout) << '\n'
|
||||
<< "spectrogram_color=" << serializeSpectrogramColor(settings.spectrogramColor) << '\n'
|
||||
<< "spectrogram_clarity=" << serializeSpectrogramClarity(settings.spectrogramClarity) << '\n'
|
||||
<< "spectrogram_scale=" << serializeSpectrogramScale(settings.spectrogramScale) << '\n'
|
||||
<< "spectrogram_orientation=" << serializeSpectrogramOrientation(settings.spectrogramOrientation) << '\n'
|
||||
<< "spectrogram_scroll=" << settings.spectrogramScrollSpeed << '\n'
|
||||
<< "spectrogram_contrast=" << settings.spectrogramContrast << '\n'
|
||||
<< "spectrogram_tilt=" << settings.spectrogramTiltDbPerOctave << '\n'
|
||||
<< "waveform_mode=" << serializeWaveformMode(settings.waveformMode) << '\n'
|
||||
<< "waveform_scroll=" << settings.waveformScrollSpeed << '\n'
|
||||
<< "waveform_multiband=" << (settings.waveformMultiband ? "true" : "false") << '\n';
|
||||
if (!output) {
|
||||
if (error) *error = "could not write settings file";
|
||||
return false;
|
||||
|
||||
@@ -18,6 +18,8 @@ enum class SettingsPage {
|
||||
Vectorscope,
|
||||
VUMeter,
|
||||
LUFSMeter,
|
||||
Spectrogram,
|
||||
Waveform,
|
||||
};
|
||||
|
||||
enum class SettingId {
|
||||
@@ -37,6 +39,16 @@ enum class SettingId {
|
||||
VUNeedleChannels,
|
||||
VUReferenceLevel,
|
||||
LUFSReadout,
|
||||
SpectrogramColor,
|
||||
SpectrogramClarity,
|
||||
SpectrogramScale,
|
||||
SpectrogramOrientation,
|
||||
SpectrogramScrollSpeed,
|
||||
SpectrogramContrast,
|
||||
SpectrogramTilt,
|
||||
WaveformMode,
|
||||
WaveformScrollSpeed,
|
||||
WaveformMultiband,
|
||||
};
|
||||
|
||||
enum class VectorscopeDetail {
|
||||
@@ -45,6 +57,12 @@ enum class VectorscopeDetail {
|
||||
Maximum,
|
||||
};
|
||||
|
||||
enum class SpectrogramColorMode { Heat, Mono };
|
||||
enum class SpectrogramClarity { Classic, Sharp, Sharper };
|
||||
enum class SpectrogramScale { Mel, Logarithmic, Linear };
|
||||
enum class SpectrogramOrientation { Horizontal, Vertical };
|
||||
enum class WaveformMode { Mono, Stereo };
|
||||
|
||||
struct TuiSettings {
|
||||
float inputTrimDb = 0.0f;
|
||||
int refreshRate = 60;
|
||||
@@ -62,6 +80,16 @@ struct TuiSettings {
|
||||
VUNeedleChannels vuNeedleChannels = VUNeedleChannels::Stereo;
|
||||
float vuReferenceDbfs = -14.0f;
|
||||
LUFSReadout lufsReadout = LUFSReadout::ShortTerm;
|
||||
SpectrogramColorMode spectrogramColor = SpectrogramColorMode::Heat;
|
||||
SpectrogramClarity spectrogramClarity = SpectrogramClarity::Sharper;
|
||||
SpectrogramScale spectrogramScale = SpectrogramScale::Logarithmic;
|
||||
SpectrogramOrientation spectrogramOrientation = SpectrogramOrientation::Horizontal;
|
||||
float spectrogramScrollSpeed = 2.0f;
|
||||
float spectrogramContrast = 1.0f;
|
||||
float spectrogramTiltDbPerOctave = 4.0f;
|
||||
WaveformMode waveformMode = WaveformMode::Mono;
|
||||
int waveformScrollSpeed = 1;
|
||||
bool waveformMultiband = false;
|
||||
};
|
||||
|
||||
struct SettingDescriptor {
|
||||
@@ -84,6 +112,12 @@ bool settingIsBoolean(SettingId setting);
|
||||
bool adjustSetting(TuiSettings& settings, SettingId setting, int direction);
|
||||
bool resetSetting(TuiSettings& settings, SettingId setting);
|
||||
|
||||
const char* spectrogramColorName(SpectrogramColorMode mode);
|
||||
const char* spectrogramClarityName(SpectrogramClarity clarity);
|
||||
const char* spectrogramScaleName(SpectrogramScale scale);
|
||||
const char* spectrogramOrientationName(SpectrogramOrientation orientation);
|
||||
const char* waveformModeName(WaveformMode mode);
|
||||
|
||||
std::filesystem::path defaultSettingsPath();
|
||||
TuiSettings loadSettings(const std::filesystem::path& path);
|
||||
bool saveSettings(const TuiSettings& settings,
|
||||
|
||||
+91
-2
@@ -4,6 +4,7 @@
|
||||
#include "display_model.h"
|
||||
#include "meter_display_model.h"
|
||||
#include "scope_plot_model.h"
|
||||
#include "scrolling_history.h"
|
||||
#include "snapshot_store.h"
|
||||
#include "spectrum_peak_model.h"
|
||||
#include "system_audio_capture.h"
|
||||
@@ -209,13 +210,23 @@ void testProjectionAndLayout() {
|
||||
Prism::Tui::PanelId::Oscilloscope,
|
||||
"panel focus should cycle forward");
|
||||
require(Prism::Tui::nextPanel(Prism::Tui::PanelId::Spectrum, true) ==
|
||||
Prism::Tui::PanelId::LUFSMeter,
|
||||
Prism::Tui::PanelId::Waveform,
|
||||
"panel focus should cycle backward");
|
||||
const auto compactPanels = Prism::Tui::visiblePanelOrder(stacked);
|
||||
require(compactPanels.size() == 3 &&
|
||||
Prism::Tui::nextPanel(
|
||||
Prism::Tui::PanelId::Spectrum, compactPanels) == Prism::Tui::PanelId::VUMeter,
|
||||
"compact layout focus should skip hidden visual scopes");
|
||||
|
||||
const auto full = Prism::Tui::buildDashboardLayout(
|
||||
140, 44, Prism::Tui::LayoutPreset::Automatic);
|
||||
require(full.panels.size() == 7 &&
|
||||
Prism::Tui::layoutContainsPanel(full, Prism::Tui::PanelId::Spectrogram) &&
|
||||
Prism::Tui::layoutContainsPanel(full, Prism::Tui::PanelId::Waveform),
|
||||
"large dashboards should compose all seven scopes at once");
|
||||
require(std::all_of(full.panels.begin(), full.panels.end(), [](const auto& panel) {
|
||||
return panel.width > 0 && panel.height > 0;
|
||||
}), "all seven panes should remain bounded after responsive layout");
|
||||
}
|
||||
|
||||
void testMeterDisplayModels() {
|
||||
@@ -279,7 +290,7 @@ void testSettingsModelAndPersistence() {
|
||||
"settings normalization should enforce public ranges");
|
||||
|
||||
const auto pages = Prism::Tui::settingsPages();
|
||||
require(pages.size() == 6 &&
|
||||
require(pages.size() == 8 &&
|
||||
Prism::Tui::settingsForPage(Prism::Tui::SettingsPage::General).size() == 3,
|
||||
"settings should expose shallow category pages");
|
||||
Prism::Tui::TuiSettings adjusted;
|
||||
@@ -312,6 +323,18 @@ void testSettingsModelAndPersistence() {
|
||||
adjusted, Prism::Tui::SettingId::LUFSReadout, 1) &&
|
||||
adjusted.lufsReadout == Prism::Tui::LUFSReadout::Integrated,
|
||||
"LUFS settings should select an independent loudness window");
|
||||
require(Prism::Tui::adjustSetting(
|
||||
adjusted, Prism::Tui::SettingId::SpectrogramClarity, -1) &&
|
||||
adjusted.spectrogramClarity == Prism::Tui::SpectrogramClarity::Sharp,
|
||||
"spectrogram settings should cycle through the native clarity modes");
|
||||
require(Prism::Tui::adjustSetting(
|
||||
adjusted, Prism::Tui::SettingId::WaveformMode, 1) &&
|
||||
adjusted.waveformMode == Prism::Tui::WaveformMode::Stereo,
|
||||
"waveform settings should expose independent mono and stereo modes");
|
||||
require(Prism::Tui::adjustSetting(
|
||||
adjusted, Prism::Tui::SettingId::WaveformMultiband, 1) &&
|
||||
adjusted.waveformMultiband,
|
||||
"waveform settings should expose the GUI multiband color mode");
|
||||
require(Prism::Tui::resetSetting(
|
||||
adjusted, Prism::Tui::SettingId::InputTrim) &&
|
||||
adjusted.inputTrimDb == 0.0f,
|
||||
@@ -480,6 +503,27 @@ void testPitchReadoutResponse() {
|
||||
"free-running oscilloscope windows should advance with every audio chunk");
|
||||
}
|
||||
|
||||
void testScrollingHistory() {
|
||||
Prism::Tui::ScrollingHistory history(2, 3);
|
||||
history.append(std::vector<float>{
|
||||
1.0f, 10.0f,
|
||||
2.0f, 20.0f,
|
||||
3.0f, 30.0f,
|
||||
4.0f, 40.0f,
|
||||
});
|
||||
const auto wrapped = history.snapshot();
|
||||
require(wrapped.columnCount == 3 && wrapped.columnStride == 2,
|
||||
"rolling histories should remain at their fixed column capacity");
|
||||
require(wrapped.values == std::vector<float>({
|
||||
2.0f, 20.0f,
|
||||
3.0f, 30.0f,
|
||||
4.0f, 40.0f,
|
||||
}), "rolling history snapshots should publish oldest-to-newest columns");
|
||||
history.reset();
|
||||
require(history.snapshot().values.empty(),
|
||||
"history reset should remove old visual data");
|
||||
}
|
||||
|
||||
void testPipelineAndFakeCapture() {
|
||||
FakeCapture capture;
|
||||
Prism::Capture::StartResult started;
|
||||
@@ -527,6 +571,35 @@ void testPipelineAndFakeCapture() {
|
||||
frame.vectorscope.multibandPoints.end(),
|
||||
[](float sample) { return std::isfinite(sample); }),
|
||||
"vectorscope samples should remain finite");
|
||||
require(frame.spectrogram.display.columnCount > 0 &&
|
||||
frame.spectrogram.display.columnStride == Prism::Tui::kSpectrogramHistoryRows &&
|
||||
frame.spectrogram.heat.columnCount == frame.spectrogram.display.columnCount,
|
||||
"the pipeline should publish synchronized spectrogram intensity histories");
|
||||
const size_t latestSpectrogramOffset =
|
||||
(frame.spectrogram.display.columnCount - 1) *
|
||||
frame.spectrogram.display.columnStride;
|
||||
const auto spectrogramBegin =
|
||||
frame.spectrogram.display.values.begin() +
|
||||
static_cast<std::ptrdiff_t>(latestSpectrogramOffset);
|
||||
const auto spectrogramEnd = spectrogramBegin +
|
||||
static_cast<std::ptrdiff_t>(frame.spectrogram.display.columnStride);
|
||||
const size_t dominantSpectrogramRow = static_cast<size_t>(std::distance(
|
||||
spectrogramBegin, std::max_element(spectrogramBegin, spectrogramEnd)));
|
||||
require(dominantSpectrogramRow > 42 && dominantSpectrogramRow < 68,
|
||||
"a deterministic 1 kHz tone should occupy the expected logarithmic spectrogram band");
|
||||
require(frame.waveform.history.columnCount > 0 &&
|
||||
frame.waveform.history.columnStride == Visualizer::WAVEFORM_STEREO_SUMMARY_STRIDE &&
|
||||
!frame.waveform.stereo,
|
||||
"the pipeline should publish a bounded mono waveform history by default");
|
||||
const size_t latestWaveformOffset =
|
||||
(frame.waveform.history.columnCount - 1) * frame.waveform.history.columnStride;
|
||||
const float* latestWaveform =
|
||||
frame.waveform.history.values.data() + latestWaveformOffset;
|
||||
require(latestWaveform[0] < -0.20f && latestWaveform[1] > 0.20f,
|
||||
"waveform columns should retain the real minimum and maximum sample envelope");
|
||||
require(std::all_of(latestWaveform + 2, latestWaveform + 5, [](float value) {
|
||||
return std::isfinite(value) && value >= 0.0f;
|
||||
}), "waveform columns should retain finite native multiband RMS values");
|
||||
require(frame.vu.barLDb > -20.0f && frame.vu.barLDb < -5.0f,
|
||||
"VU level should reflect deterministic input");
|
||||
require(std::isfinite(frame.lufs.momentaryLUFS) && frame.lufs.momentaryLUFS > -60.0f,
|
||||
@@ -548,12 +621,22 @@ void testPipelineAndFakeCapture() {
|
||||
"input trim should affect the real loudness analyzer before processing");
|
||||
|
||||
Prism::Tui::AnalysisPipeline stereoPipeline(48000.0f);
|
||||
stereoPipeline.setWaveformSettings(true, 2);
|
||||
for (int index = 0; index < 20; ++index) {
|
||||
stereoPipeline.process(stereoSineChunk(1000.0f, 0.5f, 0.125f, 2400, 48000.0f));
|
||||
}
|
||||
const auto stereo = stereoPipeline.snapshot();
|
||||
require(stereo.vu.barLDb > stereo.vu.barRDb + 10.0f,
|
||||
"stereo VU values should preserve independent channel levels");
|
||||
require(stereo.waveform.stereo && stereo.waveform.history.columnCount > 0,
|
||||
"stereo waveform mode should publish independent channel envelopes");
|
||||
const size_t stereoWaveformOffset =
|
||||
(stereo.waveform.history.columnCount - 1) *
|
||||
stereo.waveform.history.columnStride;
|
||||
const float* stereoWaveform =
|
||||
stereo.waveform.history.values.data() + stereoWaveformOffset;
|
||||
require(stereoWaveform[1] > stereoWaveform[6] * 3.0f,
|
||||
"stereo waveform envelopes should preserve independent channel amplitudes");
|
||||
pipeline.reset();
|
||||
const auto reset = pipeline.snapshot();
|
||||
require(reset.lufs.integratedLUFS <= -59.0f, "reset should clear integrated loudness");
|
||||
@@ -563,6 +646,11 @@ void testPipelineAndFakeCapture() {
|
||||
"reset should clear the fast pitch readout");
|
||||
require(reset.vectorscope.pointCount == 0 && reset.vectorscope.multibandPoints.empty(),
|
||||
"reset should clear vectorscope history");
|
||||
require(reset.spectrogram.display.columnCount == 0 &&
|
||||
reset.spectrogram.heat.columnCount == 0,
|
||||
"reset should clear both spectrogram histories");
|
||||
require(reset.waveform.history.columnCount == 0,
|
||||
"reset should clear waveform history");
|
||||
require(capture.stopped, "fake capture should stop cleanly");
|
||||
}
|
||||
|
||||
@@ -593,6 +681,7 @@ int main() {
|
||||
testSettingsModelAndPersistence();
|
||||
testScopePlotModels();
|
||||
testPitchReadoutResponse();
|
||||
testScrollingHistory();
|
||||
testPipelineAndFakeCapture();
|
||||
testThreadSafeSnapshots();
|
||||
std::cout << "Prism TUI tests passed\n";
|
||||
|
||||
Reference in New Issue
Block a user