feat: add waveform scope view with resizable divider
- Create primitives.h/c for waveform visualization (time/amplitude display) - Scope view draws signal as per-pixel min/max envelope (Audacity-style) - Add draggable divider between spectrogram and scope (30%-80% range) - Fix selection bounds to use correct spectrogram area (not full viewport) - Scope syncs with spectrogram zoom/pan for aligned time axis
This commit is contained in:
@@ -121,9 +121,11 @@ OBJECTS :=
|
|||||||
GENERATED += $(OBJDIR)/spectrogram.o
|
GENERATED += $(OBJDIR)/spectrogram.o
|
||||||
GENERATED += $(OBJDIR)/platform_linux.o
|
GENERATED += $(OBJDIR)/platform_linux.o
|
||||||
GENERATED += $(OBJDIR)/utils.o
|
GENERATED += $(OBJDIR)/utils.o
|
||||||
|
GENERATED += $(OBJDIR)/primitives.o
|
||||||
OBJECTS += $(OBJDIR)/spectrogram.o
|
OBJECTS += $(OBJDIR)/spectrogram.o
|
||||||
OBJECTS += $(OBJDIR)/platform_linux.o
|
OBJECTS += $(OBJDIR)/platform_linux.o
|
||||||
OBJECTS += $(OBJDIR)/utils.o
|
OBJECTS += $(OBJDIR)/utils.o
|
||||||
|
OBJECTS += $(OBJDIR)/primitives.o
|
||||||
|
|
||||||
# Rules
|
# Rules
|
||||||
# #############################################
|
# #############################################
|
||||||
@@ -199,6 +201,10 @@ $(OBJDIR)/utils.o: src/utils.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)/primitives.o: src/primitives.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
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#include "primitives.h"
|
||||||
|
#include "raylib.h"
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
void InitScopeView(ScopeView* view, WaveformData data, int x, int y, int width, int height)
|
||||||
|
{
|
||||||
|
view->data = data;
|
||||||
|
view->x = x;
|
||||||
|
view->y = y;
|
||||||
|
view->width = width;
|
||||||
|
view->height = height;
|
||||||
|
view->viewStart = 0.0f;
|
||||||
|
view->viewEnd = 1.0f;
|
||||||
|
view->ampMin = -1.0f;
|
||||||
|
view->ampMax = 1.0f;
|
||||||
|
view->cursorT = -1.0f; // -1 means no cursor
|
||||||
|
view->showGrid = true;
|
||||||
|
view->gridAlpha = 0.3f;
|
||||||
|
view->bgColorR = 20;
|
||||||
|
view->bgColorG = 20;
|
||||||
|
view->bgColorB = 25;
|
||||||
|
view->bgColorA = 255;
|
||||||
|
view->gridR = 80;
|
||||||
|
view->gridG = 80;
|
||||||
|
view->gridB = 100;
|
||||||
|
view->gridA = 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int AmplitudeToY(ScopeView* view, float amp)
|
||||||
|
{
|
||||||
|
return view->y + view->height - (int)((amp - view->ampMin) / (view->ampMax - view->ampMin) * view->height);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int TimeToX(ScopeView* view, float t)
|
||||||
|
{
|
||||||
|
return view->x + (int)(t * view->width);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawScopeView(ScopeView* view, float cursorT)
|
||||||
|
{
|
||||||
|
if (!view || !view->data.samples) return;
|
||||||
|
|
||||||
|
int totalSamples = view->data.numSamples;
|
||||||
|
if (totalSamples <= 0) return;
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
DrawRectangle(view->x, view->y, view->width, view->height,
|
||||||
|
(Color){ view->bgColorR, view->bgColorG, view->bgColorB, view->bgColorA });
|
||||||
|
|
||||||
|
DrawRectangleLines(view->x, view->y, view->width, view->height,
|
||||||
|
Fade((Color){ view->gridR, view->gridG, view->gridB, view->gridA }, 0.5f));
|
||||||
|
|
||||||
|
// Grid lines
|
||||||
|
if (view->showGrid) {
|
||||||
|
Color gridColor = (Color){ view->gridR, view->gridG, view->gridB, (int)(view->gridAlpha * 255) };
|
||||||
|
|
||||||
|
// Vertical time divisions
|
||||||
|
for (int i = 0; i <= 10; i++) {
|
||||||
|
int x = TimeToX(view, (float)i / 10.0f);
|
||||||
|
DrawLineV((Vector2){ x, view->y }, (Vector2){ x, view->y + view->height }, gridColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal amplitude divisions
|
||||||
|
for (int i = 0; i <= 10; i++) {
|
||||||
|
float amp = view->ampMin + (float)i / 10.0f * (view->ampMax - view->ampMin);
|
||||||
|
int y = AmplitudeToY(view, amp);
|
||||||
|
DrawLine(view->x, y, view->x + view->width, y, gridColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero line
|
||||||
|
int zeroY = AmplitudeToY(view, 0.0f);
|
||||||
|
DrawLine(view->x, zeroY, view->x + view->width, zeroY,
|
||||||
|
Fade((Color){ 255, 255, 255, 150 }, 0.7f));
|
||||||
|
|
||||||
|
// Amplitude labels
|
||||||
|
char label[32];
|
||||||
|
for (int i = 0; i <= 10; i++) {
|
||||||
|
float amp = view->ampMin + (float)i / 10.0f * (view->ampMax - view->ampMin);
|
||||||
|
int y = AmplitudeToY(view, amp);
|
||||||
|
sprintf(label, "%.1f", amp);
|
||||||
|
DrawText(label, view->x - 35, y - 5, 8,
|
||||||
|
Fade((Color){ view->gridR, view->gridG, view->gridB, view->gridA }, 0.7f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visible sample range
|
||||||
|
int startSample = (int)(view->viewStart * totalSamples);
|
||||||
|
int endSample = (int)(view->viewEnd * totalSamples);
|
||||||
|
if (startSample < 0) startSample = 0;
|
||||||
|
if (endSample > totalSamples) endSample = totalSamples;
|
||||||
|
|
||||||
|
int visibleSamples = endSample - startSample;
|
||||||
|
if (visibleSamples <= 0) return;
|
||||||
|
|
||||||
|
// Compute samples-per-pixel so every pixel gets a column
|
||||||
|
int spp = (visibleSamples + view->width - 1) / view->width;
|
||||||
|
if (spp < 1) spp = 1;
|
||||||
|
|
||||||
|
// Draw envelope: per-pixel min/max (Audacity-style)
|
||||||
|
Color waveColor = (Color){ 200, 220, 255, 255 };
|
||||||
|
for (int px = 0; px < view->width; px++) {
|
||||||
|
int s0 = startSample + px * spp;
|
||||||
|
int s1 = startSample + (px + 1) * spp;
|
||||||
|
if (s0 >= endSample) s0 = endSample - 1;
|
||||||
|
if (s1 > endSample) s1 = endSample;
|
||||||
|
|
||||||
|
float minAmp = view->data.samples[s0];
|
||||||
|
float maxAmp = view->data.samples[s0];
|
||||||
|
for (int s = s0 + 1; s < s1; s++) {
|
||||||
|
float v = view->data.samples[s];
|
||||||
|
if (v < minAmp) minAmp = v;
|
||||||
|
if (v > maxAmp) maxAmp = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
int yTop = AmplitudeToY(view, maxAmp);
|
||||||
|
int yBot = AmplitudeToY(view, minAmp);
|
||||||
|
DrawLine(px + view->x, yTop, px + view->x, yBot, waveColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor
|
||||||
|
if (cursorT >= 0.0f && cursorT <= 1.0f) {
|
||||||
|
int cursorX = TimeToX(view, cursorT);
|
||||||
|
if (cursorX >= view->x && cursorX <= view->x + view->width) {
|
||||||
|
Color cc = (Color){ 255, 80, 80, 255 };
|
||||||
|
DrawLine(cursorX, view->y, cursorX, view->y + view->height, cc);
|
||||||
|
Vector2 p1 = (Vector2){ cursorX, view->y };
|
||||||
|
Vector2 p2 = (Vector2){ cursorX - 6, view->y + 10 };
|
||||||
|
Vector2 p3 = (Vector2){ cursorX + 6, view->y + 10 };
|
||||||
|
DrawTriangle(p1, p2, p3, cc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#ifndef PRIMITIVES_H
|
||||||
|
#define PRIMITIVES_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
// Waveform data structure (mirrors AudioSignal for scope rendering)
|
||||||
|
typedef struct {
|
||||||
|
float* samples;
|
||||||
|
int numSamples;
|
||||||
|
int sampleRate;
|
||||||
|
} WaveformData;
|
||||||
|
|
||||||
|
// Scope view state for time/amplitude waveform display
|
||||||
|
typedef struct {
|
||||||
|
WaveformData data;
|
||||||
|
|
||||||
|
// View bounds (in pixels)
|
||||||
|
int x, y;
|
||||||
|
int width, height;
|
||||||
|
|
||||||
|
// Time view (0-1 normalized within the signal)
|
||||||
|
float viewStart; // start of visible time
|
||||||
|
float viewEnd; // end of visible time
|
||||||
|
|
||||||
|
// Amplitude view
|
||||||
|
float ampMin; // minimum visible amplitude (-1.0 to 1.0)
|
||||||
|
float ampMax; // maximum visible amplitude
|
||||||
|
|
||||||
|
// Cursor/selection
|
||||||
|
float cursorT; // 0-1 normalized time position of cursor
|
||||||
|
|
||||||
|
// Display settings
|
||||||
|
bool showGrid;
|
||||||
|
float gridAlpha; // 0-1 alpha for grid lines
|
||||||
|
|
||||||
|
// Background color
|
||||||
|
int bgColorR, bgColorG, bgColorB, bgColorA;
|
||||||
|
|
||||||
|
// Grid color
|
||||||
|
int gridR, gridG, gridB, gridA;
|
||||||
|
} ScopeView;
|
||||||
|
|
||||||
|
// Initialize a scope view with bounds and waveform
|
||||||
|
void InitScopeView(ScopeView* view, WaveformData data, int x, int y, int width, int height);
|
||||||
|
|
||||||
|
// Draw the scope view with optional cursor/playhead
|
||||||
|
void DrawScopeView(ScopeView* view, float cursorT);
|
||||||
|
|
||||||
|
#endif // PRIMITIVES_H
|
||||||
+135
-6
@@ -5,6 +5,7 @@
|
|||||||
#include "resource_dir.h"
|
#include "resource_dir.h"
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
#include "primitives.h"
|
||||||
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -172,6 +173,16 @@ typedef struct {
|
|||||||
// the current view range.
|
// the current view range.
|
||||||
int skipFactor;
|
int skipFactor;
|
||||||
bool highResFinished;
|
bool highResFinished;
|
||||||
|
|
||||||
|
// Waveform scope view (underneath spectrogram viewport)
|
||||||
|
ScopeView scopeView;
|
||||||
|
bool showScope; // Toggle to show/hide scope view
|
||||||
|
|
||||||
|
// Scope view divider
|
||||||
|
float dividerY; // Y position of divider between spectrogram and scope (0-1 normalized)
|
||||||
|
bool isDividing; // True while user is dragging the divider
|
||||||
|
Vector2 dividerStartPos; // Mouse position when started dividing
|
||||||
|
float dividerStartY; // Spectro height when started dividing
|
||||||
} SpectrogramApp;
|
} SpectrogramApp;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -1733,7 +1744,17 @@ int main(int argc, char* argv[])
|
|||||||
app.highResFinished = false;
|
app.highResFinished = false;
|
||||||
app.isPlaying = false;
|
app.isPlaying = false;
|
||||||
app.playbackFinished = false;
|
app.playbackFinished = false;
|
||||||
|
app.showScope = true;
|
||||||
|
app.dividerY = 0.6f; // Start with 60% spectro, 40% scope
|
||||||
|
app.isDividing = false;
|
||||||
|
app.dividerStartPos = (Vector2){ 0, 0 };
|
||||||
|
app.dividerStartY = 0;
|
||||||
|
|
||||||
|
// Initialize scope view (data synced in render loop when signal loads)
|
||||||
|
InitScopeView(&app.scopeView,
|
||||||
|
(WaveformData){app.signal.samples, app.signal.numSamples, app.signal.sampleRate},
|
||||||
|
0, 0, GetScreenWidth(), 200);
|
||||||
|
|
||||||
GenerateColormapTexture();
|
GenerateColormapTexture();
|
||||||
ScanDirectory(GetWorkingDirectory());
|
ScanDirectory(GetWorkingDirectory());
|
||||||
|
|
||||||
@@ -1802,6 +1823,11 @@ int main(int argc, char* argv[])
|
|||||||
ScanDirectory(GetWorkingDirectory());
|
ScanDirectory(GetWorkingDirectory());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scope view toggle (P key)
|
||||||
|
if (IsKeyPressed(KEY_P)) {
|
||||||
|
app.showScope = !app.showScope;
|
||||||
|
}
|
||||||
|
|
||||||
// File browser update
|
// File browser update
|
||||||
if (app.showFileBrowser) {
|
if (app.showFileBrowser) {
|
||||||
// Browser handles its own input
|
// Browser handles its own input
|
||||||
@@ -1839,11 +1865,12 @@ int main(int argc, char* argv[])
|
|||||||
float topMargin = 50 * viewScale;
|
float topMargin = 50 * viewScale;
|
||||||
float bottomMargin = 10 * viewScale;
|
float bottomMargin = 10 * viewScale;
|
||||||
|
|
||||||
|
float spectroHeight = (GetScreenHeight() - topMargin - bottomMargin - labelHeight - scrollbarHeight - 10 * viewScale) * app.dividerY;
|
||||||
Rectangle viewBounds = {
|
Rectangle viewBounds = {
|
||||||
sidebarWidth + freqLabelWidth,
|
sidebarWidth + freqLabelWidth,
|
||||||
topMargin,
|
topMargin,
|
||||||
GetScreenWidth() - sidebarWidth - freqLabelWidth - vScrollbarWidth - 20 * viewScale,
|
GetScreenWidth() - sidebarWidth - freqLabelWidth - vScrollbarWidth - 20 * viewScale,
|
||||||
GetScreenHeight() - topMargin - bottomMargin - labelHeight - scrollbarHeight - 10 * viewScale
|
spectroHeight
|
||||||
};
|
};
|
||||||
|
|
||||||
// Zoom with mouse wheel (zooms both time and frequency to maintain aspect ratio)
|
// Zoom with mouse wheel (zooms both time and frequency to maintain aspect ratio)
|
||||||
@@ -2023,14 +2050,20 @@ int main(int argc, char* argv[])
|
|||||||
float selVScrollbarWidth = 18 * selScale;
|
float selVScrollbarWidth = 18 * selScale;
|
||||||
float selTopMargin = 50 * selScale;
|
float selTopMargin = 50 * selScale;
|
||||||
float selBottomMargin = 10 * selScale;
|
float selBottomMargin = 10 * selScale;
|
||||||
|
|
||||||
|
float selSpectroHeight = (GetScreenHeight() - selTopMargin - selBottomMargin - selLabelHeight - selScrollbarHeight - 10.0f * selScale) * app.dividerY;
|
||||||
Rectangle selBounds = {
|
Rectangle selBounds = {
|
||||||
selSidebarWidth + selFreqLabelWidth,
|
selSidebarWidth + selFreqLabelWidth,
|
||||||
selTopMargin,
|
selTopMargin,
|
||||||
GetScreenWidth() - selSidebarWidth - selFreqLabelWidth - selVScrollbarWidth - 20.0f * selScale,
|
GetScreenWidth() - selSidebarWidth - selFreqLabelWidth - selVScrollbarWidth - 20.0f * selScale,
|
||||||
GetScreenHeight() - selTopMargin - selBottomMargin - selLabelHeight - selScrollbarHeight - 10.0f * selScale
|
selSpectroHeight
|
||||||
};
|
};
|
||||||
Vector2 mousePos = GetMousePosition();
|
Vector2 mousePos = GetMousePosition();
|
||||||
|
|
||||||
|
// Calculate divider screen position (for hover detection)
|
||||||
|
float dividerScreenY = selTopMargin + selSpectroHeight;
|
||||||
|
bool mouseNearDivider = mousePos.y >= (dividerScreenY - 5) && mousePos.y <= (dividerScreenY + 5) &&
|
||||||
|
mousePos.x >= selBounds.x && mousePos.x <= selBounds.x + selBounds.width;
|
||||||
|
|
||||||
// Right-click clears selection
|
// Right-click clears selection
|
||||||
if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) && CheckCollisionPointRec(mousePos, selBounds)) {
|
if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) && CheckCollisionPointRec(mousePos, selBounds)) {
|
||||||
@@ -2072,6 +2105,16 @@ int main(int argc, char* argv[])
|
|||||||
|
|
||||||
// LMB drag = box select (time + frequency) OR drag existing selection
|
// LMB drag = box select (time + frequency) OR drag existing selection
|
||||||
if (app.loaded && !app.showFileBrowser && CheckCollisionPointRec(mousePos, selBounds)) {
|
if (app.loaded && !app.showFileBrowser && CheckCollisionPointRec(mousePos, selBounds)) {
|
||||||
|
// Set cursor to resize all when near divider
|
||||||
|
if (mouseNearDivider && !app.isDividing) {
|
||||||
|
SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL);
|
||||||
|
} else if (app.isDraggingSelection) {
|
||||||
|
SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL); // 4-way arrow while dragging
|
||||||
|
} else if (hoverInsideSelection) {
|
||||||
|
SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); // Pointing hand on hover
|
||||||
|
} else {
|
||||||
|
SetMouseCursor(MOUSE_CURSOR_DEFAULT); // Normal arrow
|
||||||
|
}
|
||||||
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
||||||
if (clickInsideSelection) {
|
if (clickInsideSelection) {
|
||||||
// Start dragging existing selection
|
// Start dragging existing selection
|
||||||
@@ -2157,7 +2200,35 @@ int main(int argc, char* argv[])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle divider drag
|
||||||
|
if (app.loaded && app.showScope && !app.showFileBrowser) {
|
||||||
|
// Calculate divider position (using same calculation as selBounds)
|
||||||
|
float viewScale = GetUIScale();
|
||||||
|
float topMargin = 50 * viewScale;
|
||||||
|
float bottomMargin = 10 * viewScale;
|
||||||
|
float labelHeight = 15 * viewScale;
|
||||||
|
float scrollbarHeight = 18 * viewScale;
|
||||||
|
float totalHeight = GetScreenHeight() - topMargin - bottomMargin - labelHeight - scrollbarHeight;
|
||||||
|
|
||||||
|
// Check if mouse is near divider
|
||||||
|
if (mouseNearDivider && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
||||||
|
app.isDividing = true;
|
||||||
|
app.dividerStartPos = mousePos;
|
||||||
|
app.dividerStartY = app.dividerY;
|
||||||
|
}
|
||||||
|
if (app.isDividing && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
|
||||||
|
float dy = (mousePos.y - app.dividerStartPos.y) / GetScreenHeight();
|
||||||
|
app.dividerY = app.dividerStartY + dy;
|
||||||
|
// Clamp to reasonable range: 0.3 to 0.8 (30% to 80% for spectrogram)
|
||||||
|
if (app.dividerY < 0.3f) app.dividerY = 0.3f;
|
||||||
|
if (app.dividerY > 0.8f) app.dividerY = 0.8f;
|
||||||
|
}
|
||||||
|
if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
|
||||||
|
app.isDividing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Processing (incremental across frames)
|
// Processing (incremental across frames)
|
||||||
if (app.loaded && !app.stftComputed) {
|
if (app.loaded && !app.stftComputed) {
|
||||||
if (app.loadingPhase == 0) {
|
if (app.loadingPhase == 0) {
|
||||||
@@ -2258,11 +2329,12 @@ int main(int argc, char* argv[])
|
|||||||
float bottomMargin = 10 * renderScale;
|
float bottomMargin = 10 * renderScale;
|
||||||
|
|
||||||
// Spectrogram area (excludes labels and scrollbars)
|
// Spectrogram area (excludes labels and scrollbars)
|
||||||
|
float spectroHeight = (GetScreenHeight() - topMargin - bottomMargin - labelHeight - scrollbarHeight - 10 * renderScale) * app.dividerY;
|
||||||
Rectangle viewBounds = {
|
Rectangle viewBounds = {
|
||||||
sidebarWidth + freqLabelWidth,
|
sidebarWidth + freqLabelWidth,
|
||||||
topMargin,
|
topMargin,
|
||||||
GetScreenWidth() - sidebarWidth - freqLabelWidth - vScrollbarWidth - 20 * renderScale,
|
GetScreenWidth() - sidebarWidth - freqLabelWidth - vScrollbarWidth - 20 * renderScale,
|
||||||
GetScreenHeight() - topMargin - bottomMargin - labelHeight - scrollbarHeight - 10 * renderScale
|
spectroHeight
|
||||||
};
|
};
|
||||||
// Time labels sit just below the spectrogram
|
// Time labels sit just below the spectrogram
|
||||||
Rectangle timeLabelArea = { viewBounds.x, viewBounds.y + viewBounds.height, viewBounds.width, labelHeight };
|
Rectangle timeLabelArea = { viewBounds.x, viewBounds.y + viewBounds.height, viewBounds.width, labelHeight };
|
||||||
@@ -2397,6 +2469,63 @@ int main(int argc, char* argv[])
|
|||||||
float freqMax = app.freqViewEnd * maxFreq;
|
float freqMax = app.freqViewEnd * maxFreq;
|
||||||
DrawTextScaled(TextFormat("Freq: %.0f-%.0f Hz", freqMin, freqMax),
|
DrawTextScaled(TextFormat("Freq: %.0f-%.0f Hz", freqMin, freqMax),
|
||||||
viewBounds.x, viewBounds.y - 30, 20, LIGHTGRAY);
|
viewBounds.x, viewBounds.y - 30, 20, LIGHTGRAY);
|
||||||
|
|
||||||
|
// Draw waveform scope view underneath the spectrogram
|
||||||
|
if (app.showScope && app.loaded && app.signal.samples != NULL) {
|
||||||
|
float totalArea = GetScreenHeight() - topMargin - bottomMargin - labelHeight - scrollbarHeight - 10 * renderScale;
|
||||||
|
float scopeHeight = totalArea * (1.0f - app.dividerY) - 30 * renderScale;
|
||||||
|
app.scopeView.y = viewBounds.y + viewBounds.height + 30;
|
||||||
|
app.scopeView.x = viewBounds.x;
|
||||||
|
app.scopeView.width = viewBounds.width;
|
||||||
|
app.scopeView.height = (int)scopeHeight;
|
||||||
|
// Keep time view in sync with spectrogram view
|
||||||
|
app.scopeView.viewStart = app.viewStart;
|
||||||
|
app.scopeView.viewEnd = app.viewEnd;
|
||||||
|
// Update waveform data
|
||||||
|
app.scopeView.data.samples = app.signal.samples;
|
||||||
|
app.scopeView.data.numSamples = app.signal.numSamples;
|
||||||
|
app.scopeView.data.sampleRate = app.signal.sampleRate;
|
||||||
|
// Show playhead if playing
|
||||||
|
if (app.isPlaying) {
|
||||||
|
DrawScopeView(&app.scopeView, app.timeSelectionStart + app.playheadT * (app.timeSelectionEnd - app.timeSelectionStart));
|
||||||
|
} else {
|
||||||
|
DrawScopeView(&app.scopeView, -1.0f);
|
||||||
|
}
|
||||||
|
// Draw scope label
|
||||||
|
DrawTextScaled("Waveform (Time/Amplitude)", viewBounds.x, app.scopeView.y - 20, 14, LIGHTGRAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw divider line between spectrogram and scope
|
||||||
|
if (app.showScope && app.loaded) {
|
||||||
|
float dividerY = viewBounds.y + viewBounds.height;
|
||||||
|
Color dividerColor = app.isDividing ? CYAN : Fade((Color){ 180, 180, 200, 255 }, 0.7f);
|
||||||
|
|
||||||
|
// Draw divider with handle
|
||||||
|
int handleW = 40;
|
||||||
|
int handleH = 10;
|
||||||
|
int handleX = viewBounds.x + viewBounds.width / 2 - handleW / 2;
|
||||||
|
int handleY = (int)dividerY - handleH / 2;
|
||||||
|
|
||||||
|
if (app.isDividing) {
|
||||||
|
// Highlighted handle while dragging
|
||||||
|
DrawRectangle(handleX, handleY, handleW, handleH, CYAN);
|
||||||
|
DrawRectangleLines(handleX, handleY, handleW, handleH, WHITE);
|
||||||
|
} else {
|
||||||
|
// Normal handle
|
||||||
|
DrawRectangle(handleX, handleY, handleW, handleH, GRAY);
|
||||||
|
DrawRectangleLines(handleX, handleY, handleW, handleH, Fade(YELLOW, 0.6f));
|
||||||
|
|
||||||
|
// Draw handle grips (3 dots)
|
||||||
|
Color gripColor = Fade(WHITE, 0.6f);
|
||||||
|
DrawPixel(handleX + handleW / 3, handleY + handleH / 2, gripColor);
|
||||||
|
DrawPixel(handleX + handleW / 2, handleY + handleH / 2, gripColor);
|
||||||
|
DrawPixel(handleX + handleW * 2 / 3, handleY + handleH / 2, gripColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw line extending from handle to edges
|
||||||
|
DrawLine(viewBounds.x, (int)dividerY, handleX, (int)dividerY, dividerColor);
|
||||||
|
DrawLine(handleX + handleW, (int)dividerY, viewBounds.x + viewBounds.width, (int)dividerY, dividerColor);
|
||||||
|
}
|
||||||
} else if (!app.showFileBrowser) {
|
} else if (!app.showFileBrowser) {
|
||||||
const char* msg1 = "Press 'O' or click 'Open File Browser' to load a WAV";
|
const char* msg1 = "Press 'O' or click 'Open File Browser' to load a WAV";
|
||||||
const char* msg2 = "Or drag & drop a file, or use: ./rspektrum <file.wav>";
|
const char* msg2 = "Or drag & drop a file, or use: ./rspektrum <file.wav>";
|
||||||
|
|||||||
Reference in New Issue
Block a user