diff --git a/.qwen/settings.json b/.qwen/settings.json new file mode 100644 index 0000000..135b826 --- /dev/null +++ b/.qwen/settings.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(make)" + ] + }, + "$version": 4 +} \ No newline at end of file diff --git a/.qwen/settings.json.orig b/.qwen/settings.json.orig new file mode 100644 index 0000000..398d16a --- /dev/null +++ b/.qwen/settings.json.orig @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(make)" + ] + } +} \ No newline at end of file diff --git a/rspektrum.make b/rspektrum.make index b07652b..fcb333a 100644 --- a/rspektrum.make +++ b/rspektrum.make @@ -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 diff --git a/spectrogram_export.png b/spectrogram_export.png new file mode 100644 index 0000000..a200fbf Binary files /dev/null and b/spectrogram_export.png differ diff --git a/spectrogram_full.png b/spectrogram_full.png new file mode 100644 index 0000000..ad0121b Binary files /dev/null and b/spectrogram_full.png differ diff --git a/src/spectrogram.c b/src/spectrogram.c index e731a19..86d44d0 100644 --- a/src/spectrogram.c +++ b/src/spectrogram.c @@ -4,6 +4,7 @@ #include "raylib.h" #include "resource_dir.h" #include "platform.h" +#include "utils.h" #include #include @@ -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); + } + } + } } // ============================================================================ diff --git a/src/utils.c b/src/utils.c new file mode 100644 index 0000000..93c798b --- /dev/null +++ b/src/utils.c @@ -0,0 +1,65 @@ +// Signal analysis utilities for spectrogram selection stats + +#include +#include +#include + +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; +} diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 0000000..249f86b --- /dev/null +++ b/src/utils.h @@ -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