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:
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(make)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"$version": 4
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(make)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -120,8 +120,10 @@ OBJECTS :=
|
|||||||
|
|
||||||
GENERATED += $(OBJDIR)/spectrogram.o
|
GENERATED += $(OBJDIR)/spectrogram.o
|
||||||
GENERATED += $(OBJDIR)/platform_linux.o
|
GENERATED += $(OBJDIR)/platform_linux.o
|
||||||
|
GENERATED += $(OBJDIR)/utils.o
|
||||||
OBJECTS += $(OBJDIR)/spectrogram.o
|
OBJECTS += $(OBJDIR)/spectrogram.o
|
||||||
OBJECTS += $(OBJDIR)/platform_linux.o
|
OBJECTS += $(OBJDIR)/platform_linux.o
|
||||||
|
OBJECTS += $(OBJDIR)/utils.o
|
||||||
|
|
||||||
# Rules
|
# Rules
|
||||||
# #############################################
|
# #############################################
|
||||||
@@ -193,6 +195,10 @@ $(OBJDIR)/platform_linux.o: src/platform_linux.c
|
|||||||
@echo "$(notdir $<)"
|
@echo "$(notdir $<)"
|
||||||
$(SILENT) $(CC) $(ALL_CFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<"
|
$(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)
|
-include $(OBJECTS:%.o=%.d)
|
||||||
ifneq (,$(PCH))
|
ifneq (,$(PCH))
|
||||||
-include $(PCH_PLACEHOLDER).d
|
-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
@@ -4,6 +4,7 @@
|
|||||||
#include "raylib.h"
|
#include "raylib.h"
|
||||||
#include "resource_dir.h"
|
#include "resource_dir.h"
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -64,14 +65,6 @@ typedef enum {
|
|||||||
// Data Structures
|
// Data Structures
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
float* samples;
|
|
||||||
int numSamples;
|
|
||||||
int sampleRate;
|
|
||||||
int channels;
|
|
||||||
float duration;
|
|
||||||
} AudioSignal;
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
float frequency;
|
float frequency;
|
||||||
float amplitude;
|
float amplitude;
|
||||||
@@ -1210,6 +1203,70 @@ static void DrawSelection(Rectangle bounds)
|
|||||||
|
|
||||||
// Draw selection box border
|
// Draw selection box border
|
||||||
DrawRectangleLinesEx((Rectangle){ selStartX, selStartY, selEndX - selStartX, selEndY - selStartY }, 2, YELLOW);
|
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)
|
static void DrawSelectionDrag(Rectangle bounds)
|
||||||
@@ -1240,6 +1297,70 @@ static void DrawSelectionDrag(Rectangle bounds)
|
|||||||
float h = fabsf(selEndY - selStartY);
|
float h = fabsf(selEndY - selStartY);
|
||||||
|
|
||||||
DrawRectangleLinesEx((Rectangle){ x, y, w, h }, 2, YELLOW);
|
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
@@ -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
@@ -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
|
||||||
Reference in New Issue
Block a user