feat: add signal stats panel for selection region

Display Duration, Energy, Peak amplitude, RMS amplitude, and PAPR for
the current time/frequency selection. The stats box:
- Appears next to the selection box (right side, or left if not enough room)
- Centers vertically on the selection box
- Uses a semi-transparent dark background with yellow border
- Updates live while dragging the selection
- Computed in utils.c with ComputeSignalStats()
This commit is contained in:
2026-05-24 22:49:44 -07:00
parent 0c64202670
commit cc58965acc
8 changed files with 241 additions and 8 deletions
+8
View File
@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(make)"
]
},
"$version": 4
}
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(make)"
]
}
}
+6
View File
@@ -120,8 +120,10 @@ OBJECTS :=
GENERATED += $(OBJDIR)/spectrogram.o
GENERATED += $(OBJDIR)/platform_linux.o
GENERATED += $(OBJDIR)/utils.o
OBJECTS += $(OBJDIR)/spectrogram.o
OBJECTS += $(OBJDIR)/platform_linux.o
OBJECTS += $(OBJDIR)/utils.o
# Rules
# #############################################
@@ -193,6 +195,10 @@ $(OBJDIR)/platform_linux.o: src/platform_linux.c
@echo "$(notdir $<)"
$(SILENT) $(CC) $(ALL_CFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<"
$(OBJDIR)/utils.o: src/utils.c
@echo "$(notdir $<)"
$(SILENT) $(CC) $(ALL_CFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<"
-include $(OBJECTS:%.o=%.d)
ifneq (,$(PCH))
-include $(PCH_PLACEHOLDER).d
Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

+129 -8
View File
@@ -4,6 +4,7 @@
#include "raylib.h"
#include "resource_dir.h"
#include "platform.h"
#include "utils.h"
#include <stdlib.h>
#include <string.h>
@@ -64,14 +65,6 @@ typedef enum {
// Data Structures
// ============================================================================
typedef struct {
float* samples;
int numSamples;
int sampleRate;
int channels;
float duration;
} AudioSignal;
typedef struct {
float frequency;
float amplitude;
@@ -1210,6 +1203,70 @@ static void DrawSelection(Rectangle bounds)
// Draw selection box border
DrawRectangleLinesEx((Rectangle){ selStartX, selStartY, selEndX - selStartX, selEndY - selStartY }, 2, YELLOW);
// Display selection stats inside viewport, clamped to fit
{
int startSample = (int)(app.timeSelectionStart * app.signal.numSamples);
int endSample = (int)(app.timeSelectionEnd * app.signal.numSamples);
SignalStats stats = ComputeSignalStats(&app.signal, startSample, endSample);
if (stats.durationSec > 0.0f && app.signal.samples != NULL) {
char lines[5][128];
int lineCount = 0;
int fontSize = 10;
int maxTextW = 0;
sprintf(lines[lineCount++], "Duration: %.3fs", stats.durationSec);
sprintf(lines[lineCount++], "Energy: %.2f", stats.energy);
sprintf(lines[lineCount++], "Peak: %.3f", stats.peakAmplitude);
sprintf(lines[lineCount++], "RMS: %.3f", stats.rmsAmplitude);
sprintf(lines[lineCount++], "PAPR: %.1f dB", stats.paprDb);
// Measure text width
for (int i = 0; i < lineCount; i++) {
int textW = MeasureText(lines[i], fontSize);
if (textW > maxTextW) maxTextW = textW;
}
int boxW = maxTextW + 20;
int boxH = lineCount * 14 + 12;
// Center vertically on the selection box, clamp to viewport
float selCenterY = (selStartY + selEndY) / 2.0f;
float boxY = selCenterY - boxH / 2.0f;
if (boxY < bounds.y) boxY = bounds.y;
if (boxY + boxH > bounds.y + bounds.height) boxY = bounds.y + bounds.height - boxH;
// Place to the right of the selection, or left if not enough room
float boxX = selEndX + 10;
if (boxX + boxW > bounds.x + bounds.width) {
boxX = selStartX - boxW - 10;
if (boxX < bounds.x) boxX = bounds.x;
}
// Clamp to viewport bounds
if (boxX + boxW > bounds.x + bounds.width) {
// Not enough room on right — draw to the left of selection
if (selStartX > boxW + 20) {
boxX = selStartX - boxW - 10;
} else {
boxX = bounds.x;
}
}
if (boxY + boxH > bounds.y + bounds.height) {
boxY = bounds.y + bounds.height - boxH;
}
// Draw background box
DrawRectangle((int)boxX, (int)boxY, boxW, boxH, (Color){ 0, 0, 0, 200 });
DrawRectangleLines((int)boxX, (int)boxY, boxW, boxH, Fade(YELLOW, 0.6f));
// Draw text
for (int i = 0; i < lineCount; i++) {
DrawText(lines[i], (int)boxX + 10, (int)boxY + 8 + i * 14, fontSize, LIGHTGRAY);
}
}
}
}
static void DrawSelectionDrag(Rectangle bounds)
@@ -1240,6 +1297,70 @@ static void DrawSelectionDrag(Rectangle bounds)
float h = fabsf(selEndY - selStartY);
DrawRectangleLinesEx((Rectangle){ x, y, w, h }, 2, YELLOW);
// Display live stats while dragging (inside viewport, clamped to fit)
{
int startSample = (int)(app.timeSelectionStart * app.signal.numSamples);
int endSample = (int)(app.timeSelectionEnd * app.signal.numSamples);
SignalStats stats = ComputeSignalStats(&app.signal, startSample, endSample);
if (stats.durationSec > 0.0f && app.signal.samples != NULL) {
char lines[5][128];
int lineCount = 0;
int fontSize = 10;
int maxTextW = 0;
sprintf(lines[lineCount++], "Duration: %.3fs", stats.durationSec);
sprintf(lines[lineCount++], "Energy: %.2f", stats.energy);
sprintf(lines[lineCount++], "Peak: %.3f", stats.peakAmplitude);
sprintf(lines[lineCount++], "RMS: %.3f", stats.rmsAmplitude);
sprintf(lines[lineCount++], "PAPR: %.1f dB", stats.paprDb);
// Measure text width
for (int i = 0; i < lineCount; i++) {
int textW = MeasureText(lines[i], fontSize);
if (textW > maxTextW) maxTextW = textW;
}
int boxW = maxTextW + 20;
int boxH = lineCount * 14 + 12;
// Center vertically on the selection box, clamp to viewport
float selCenterY = (selStartY + selEndY) / 2.0f;
float boxY = selCenterY - boxH / 2.0f;
if (boxY < bounds.y) boxY = bounds.y;
if (boxY + boxH > bounds.y + bounds.height) boxY = bounds.y + bounds.height - boxH;
// Place to the right of the selection, or left if not enough room
float boxX = selEndX + 10;
if (boxX + boxW > bounds.x + bounds.width) {
boxX = selStartX - boxW - 10;
if (boxX < bounds.x) boxX = bounds.x;
}
// Clamp to viewport bounds
if (boxX + boxW > bounds.x + bounds.width) {
// Not enough room on right — draw to the left of selection
if (x > boxW + 20) {
boxX = x - boxW - 10;
} else {
boxX = bounds.x;
}
}
if (boxY + boxH > bounds.y + bounds.height) {
boxY = bounds.y + bounds.height - boxH;
}
// Draw background box
DrawRectangle((int)boxX, (int)boxY, boxW, boxH, (Color){ 0, 0, 0, 200 });
DrawRectangleLines((int)boxX, (int)boxY, boxW, boxH, Fade(YELLOW, 0.6f));
// Draw text
for (int i = 0; i < lineCount; i++) {
DrawText(lines[i], (int)boxX + 10, (int)boxY + 8 + i * 14, fontSize, LIGHTGRAY);
}
}
}
}
// ============================================================================
+65
View File
@@ -0,0 +1,65 @@
// Signal analysis utilities for spectrogram selection stats
#include <math.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
float* samples;
int numSamples;
int sampleRate;
int channels;
float duration;
} AudioSignal;
typedef struct {
float durationSec;
float energy;
float peakAmplitude;
float rmsAmplitude;
float paprDb; // Peak-to-Average Power Ratio in dB
int peakSampleIdx; // sample index of peak amplitude
} SignalStats;
SignalStats ComputeSignalStats(const AudioSignal* signal, int startSample, int endSample)
{
SignalStats stats = {0};
if (!signal || !signal->samples || endSample <= startSample ||
startSample < 0 || endSample > signal->numSamples) {
return stats;
}
int numSamples = endSample - startSample;
float* samples = signal->samples + startSample;
float peak = 0.0f;
float energy = 0.0f;
int peakIdx = 0;
// Compute energy and find peak
for (int i = 0; i < numSamples; i++) {
float amp = samples[i];
energy += amp * amp;
if (fabsf(amp) > peak) {
peak = fabsf(amp);
peakIdx = i;
}
}
stats.energy = energy;
stats.peakAmplitude = peak;
stats.rmsAmplitude = sqrtf(energy / numSamples);
stats.durationSec = (float)numSamples / (float)signal->sampleRate;
stats.peakSampleIdx = startSample + peakIdx;
// PAPR in dB: 10 * log10(peak^2 / avg_power)
// avg_power = energy / numSamples = rms^2
// So PAPR = 10 * log10(peak^2 / rms^2) = 20 * log10(peak / rms)
if (stats.rmsAmplitude > 0.0f) {
stats.paprDb = 10.0f * log10f((peak * peak) / (stats.rmsAmplitude * stats.rmsAmplitude));
} else {
stats.paprDb = 0.0f;
}
return stats;
}
+26
View File
@@ -0,0 +1,26 @@
#ifndef SIGNAL_UTILS_H
#define SIGNAL_UTILS_H
// Structure mirroring AudioSignal from spectrogram.c
// (kept separate to avoid circular dependencies and coupling)
typedef struct {
float* samples;
int numSamples;
int sampleRate;
int channels;
float duration;
} AudioSignal;
typedef struct {
float durationSec;
float energy;
float peakAmplitude;
float rmsAmplitude;
float paprDb; // Peak-to-Average Power Ratio in dB
int peakSampleIdx; // sample index of peak amplitude
} SignalStats;
// Compute statistics for a region of the signal defined by sample range
SignalStats ComputeSignalStats(const AudioSignal* signal, int startSample, int endSample);
#endif // SIGNAL_UTILS_H