Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ jobs:
./test_pitch
"$CC" -O2 -std=c99 -Wall -o test_lufs tests/test_lufs.c -lm
./test_lufs
"$CC" -O2 -std=c99 -Wall -o test_prod tests/test_prod.c -lm
./test_prod

# Build smoke: make sure the plugin actually compiles on each platform.
build-linux:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ vlc-3.0.23-include/
*.so
*.o

# local private production-reference import
PRODUCTION_PLAYBOOK.md

.DS_Store
7 changes: 5 additions & 2 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
A second view of the GitHub **Issues** tab. Every item here ↔ a matching issue (numbers linked once filed).

## Open
_(nothing open)_
- [ ] **Tempo / beat-grid detection** — derive BPM, onset grid, swing/late/early feel, and 4/8/16-bar hypermeasure cues from the playbook groove sections.
- [ ] **Chord / harmonic-function detection** — add chord and function hints on top of chroma/key for arrangement and reharmonization decisions.
- [ ] **Sound-design diagnostics** — add transient/envelope, spectral-motion, modulation, and timbre panels for synthesis/FX checks.

## Done
- [x] **In-window view switcher** ([#15](https://github.com/CryptoJones/VibeComposing-Analyzer/issues/15)) — clickable tabs across the top of every layout (`DASH · KEYS · SPECTRO · SPECTRUM · WAVE · STEREO`); switch views with a mouse click, no VLC menus.
- [x] **Production QA view** — `PROD` tab and `--dix-layout=production`: tonal balance, LUFS targets, crest/headroom, low-end mono safety, per-band width, and masking watch grounded in the As30p production playbook.
- [x] **In-window view switcher** ([#15](https://github.com/CryptoJones/VibeComposing-Analyzer/issues/15)) — clickable tabs across the top of every layout (`DASH · PROD · KEYS (COMPOSE) · SPECTRO · SPECTRUM · WAVE · STEREO`); switch views with a mouse click, no VLC menus.
- [x] **GUI-launch discovery** ([#14](https://github.com/CryptoJones/VibeComposing-Analyzer/pull/14)) — install into VLC.app's bundle so Dock/Finder/Start-menu launches list Dix without an env var.
- [x] **True LUFS** — ITU-R BS.1770-4 K-weighting + gated integrated / short-term / momentary (`lufs` panel; `dix_lufs.h`, unit-tested).
- [x] **Custom layouts** — `--dix-dash="a,b,c,…"`: pick any panels (engineering + composition), auto-arranged into a grid.
Expand Down
39 changes: 29 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
[![GitHub](https://img.shields.io/badge/GitHub-CryptoJones%2FVibeComposing--Analyzer-181717?logo=github&logoColor=white)](https://github.com/CryptoJones/VibeComposing-Analyzer)
[![C99](https://img.shields.io/badge/C-C99-A8B9CC?logo=c&logoColor=white)](waveform.c)
[![VLC 3.0.x](https://img.shields.io/badge/VLC-3.0.x-FF8800?logo=vlcmediaplayer&logoColor=white)](https://www.videolan.org/vlc/)
[![Version](https://img.shields.io/badge/version-3.5.4-orange)](https://github.com/CryptoJones/VibeComposing-Analyzer)
[![Version](https://img.shields.io/badge/version-3.6.0-orange)](https://github.com/CryptoJones/VibeComposing-Analyzer)

> **Sister project:** [**FL-Studio-MCP-Server**](https://github.com/CryptoJones/FL-Studio-MCP-Server)
> — an MCP server that lets Claude Code drive FL Studio and generate editable `.flp` projects. Where this
Expand All @@ -28,13 +28,16 @@ Module name: `dixwaveform` · target: **VLC 3.0.x**, macOS **arm64** (portable t
- Reads the live 32-bit-float PCM buffer, computes real-time analysis, renders a 1280x720 analyzer frame, and **passes the audio through untouched**.
- Shows engineering views: scrolling log spectrogram, FFT magnitude/peak-hold spectrum, stereo waveform/oscilloscope, and stereo phase + peak/RMS/crest/clip meters.
- Plus a **`compose` view** that *hears the music*: a live chromagram (12 pitch classes), circle-of-fifths **key detection**, a nearest-note + cents tuning readout, and an **energy/build-drop ribbon** — grounded in the Dix music library (pitch math from *Mathematics and Music*; groove/structure from *Unlocking the Groove*).
- **Switch views in-window** — a row of clickable tabs across the top (`DASH · KEYS (COMPOSE) · SPECTRO · SPECTRUM · WAVE · STEREO`) lets you jump between layouts with a mouse click, no VLC menus. (`KEYS (COMPOSE)` = the key/compose view.)
- Adds a **`production` view** that turns the As30p production playbook into mix/master QA: tonal-balance bands, loudness targets, crest/headroom, low-end mono safety, per-band width, and masking risk.
- **Switch views in-window** — a row of clickable tabs across the top (`DASH · PROD · KEYS (COMPOSE) · SPECTRO · SPECTRUM · WAVE · STEREO`) lets you jump between layouts with a mouse click, no VLC menus. (`KEYS (COMPOSE)` = the key/compose view.)
- Selectable via **Audio ▸ Visualizations ▸ Dix Analyzer**, or `--audio-visual=dixwaveform`.

## The six views
## The seven views

Click a tab to switch — no VLC menus. (Screenshots: As30p feat. The Dixie Flatline — *Antigua Runners Theme (OST)*.)

**PROD** — the production QA view: tonal balance, LUFS targets, crest/headroom, low-end mono safety, per-band width, and masking watch.

**KEYS (COMPOSE)** — the musical view: nearest note + cents, a 12-bin chromagram, the estimated key on a circle-of-fifths wheel, and an energy build/drop ribbon.
![KEYS (COMPOSE) view](docs/screenshots/dix-keys-compose.png)

Expand Down Expand Up @@ -113,17 +116,18 @@ vlc --audio-visual=dixwaveform \
yourtrack.mp3
```

- `--dix-layout=dashboard|spectrogram|waveform|spectrum|stereo|compose`
- `--dix-layout=dashboard|spectrogram|waveform|spectrum|stereo|compose|production`
- `compose` — chromagram, circle-of-fifths key, note/cents readout, and energy/build-drop ribbon.
- `production` — playbook-derived mix QA: tonal balance, LUFS targets, low-end mono, per-band width, crest/headroom, and masking watch.
- `--dix-fft=1024|2048|4096|8192`
- `--dix-scale=log|linear`
- `--dix-channels=lr|ms|left|right`
- `--dix-speed=slow|normal|fast`
- `--dix-palette=warm|classic|mono`
- `--dix-reference=off|avg|tilt|both` — overlay a reference on the spectrum: `avg` = the track's long-term-average curve, `tilt` = a −3 dB/oct "pink" target slope, `both`. A quick tonal-balance check.
- `--dix-dash="a,b,c,…"` — **custom dashboard**: a comma list of panels auto-arranged into a grid, mixing engineering and composition panels freely. Panels: `spectrogram, spectrum, waveform, stereo, chroma, key, note, energy, lufs` (up to 6). Empty = the default dashboard. Example: `--dix-dash="spectrum,lufs,stereo,energy"`.
- `--dix-dash="a,b,c,…"` — **custom dashboard**: a comma list of panels auto-arranged into a grid, mixing engineering, composition, and production panels freely. Panels: `spectrogram, spectrum, waveform, stereo, chroma, key, note, energy, lufs, tonal, lowmono, width, crest, mask` (up to 6). Empty = the default dashboard. Example: `--dix-dash="spectrum,lufs,tonal,width"`.

The **`lufs`** panel is a real ITU-R BS.1770-4 / EBU R128 loudness meter: momentary (400 ms), short-term (3 s), and gated integrated LUFS, with −14 (streaming) and −23 (broadcast) target ticks. K-weighting coefficients are derived for the actual sample rate; the engine (`dix_lufs.h`) is unit-tested.
The **`lufs`** panel is a real ITU-R BS.1770-4 / EBU R128 loudness meter: momentary (400 ms), short-term (3 s), and gated integrated LUFS, with −14 (streaming), −10..−9 (club/SoundCloud), and −23 (broadcast) target ticks. K-weighting coefficients are derived for the actual sample rate; the engine (`dix_lufs.h`) is unit-tested.
- `--dix-title="Track Name"` draws title text after `DIX ANALYZER` in the header.

## The `compose` view
Expand All @@ -141,12 +145,26 @@ The music theory is grounded in the Dix library: pitch math (A440, `midi = 69 +

**Access it:** click the **KEYS (COMPOSE)** tab at the top of the Dix window, launch with `--dix-layout=compose`, or set the layout in the plugin's advanced preferences.

## The `production` view

`--dix-layout=production` turns the analyzer into a real-time production QA surface grounded in the As30p production playbook:

| Panel | What it shows | Playbook check |
| --- | --- | --- |
| **TONAL BALANCE** | Sub, low, mud, body, presence, harsh, air, and top bands relative to full-band average. | Pink-ish contour, cut mud, watch harshness, keep air controlled. |
| **LOUDNESS** | Momentary, short-term, and gated integrated LUFS. | Streaming `-14 LUFS`; club/SoundCloud `-10..-9 LUFS`; broadcast `-23 LUFS`. |
| **CREST HEADROOM** | Crest factor, sample peak, and integrated LUFS readout. | Preserve 8-12 dB crest and avoid hot sample peaks. |
| **LOW MONO** | Low-band correlation and side-width. | Kick, bass, and sub stay mono-centered. |
| **WIDTH BY BAND** | Low/mid/high correlation plus side share. | Low end narrow, highs may widen, correlation stays above zero. |
| **MASK WATCH** | Mud, vocal presence, harshness, and air risk bands. | Pan first, mirror EQ, protect vocal presence, cut narrow and boost wide. |

## Tests & CI

- **Unit tests** cover the pitch/chroma/key DSP (`dix_pitch.h`) and the BS.1770 loudness engine (`dix_lufs.h`) — Hz→note, cents, key detection, diatonic membership, K-weighting coefficients, level scaling, and gated integrated loudness. Run locally:
- **Unit tests** cover the pitch/chroma/key DSP (`dix_pitch.h`), the BS.1770 loudness engine (`dix_lufs.h`), and the production QA helpers (`dix_prod.h`) — Hz→note, cents, key detection, diatonic membership, K-weighting coefficients, level scaling, gated integrated loudness, band power, stereo correlation, width, and crest math. Run locally:
```sh
cc -O2 -std=c99 -o test_pitch tests/test_pitch.c -lm && ./test_pitch
cc -O2 -std=c99 -o test_lufs tests/test_lufs.c -lm && ./test_lufs
cc -O2 -std=c99 -o test_prod tests/test_prod.c -lm && ./test_prod
```
- **CI** (`.github/workflows/ci.yml`) runs those unit tests **and** a plugin build-smoke on **Linux, macOS, and Windows** for every push/PR to `main`. Release binaries for all three are published on `v*` tags (`release.yml`).
- **UI is verified interactively on macOS** (arm64). The Linux/Windows *builds* are exercised in CI, but interactive UI on those platforms is not yet part of automated testing.
Expand All @@ -155,17 +173,18 @@ The music theory is grounded in the Dix library: pitch math (A440, `midi = 69 +
- `waveform.c` — the plugin: descriptor + `Open`/`DoWork`/`Close`, FFT analysis, meters, composition analysis, RGB dashboard renderer, and I420 conversion.
- `dix_pitch.h` — pure, VLC-independent pitch/chroma/key DSP (unit-tested standalone).
- `dix_lufs.h` — ITU-R BS.1770 loudness engine (unit-tested standalone).
- `tests/test_pitch.c`, `tests/test_lufs.c` — unit tests for the DSP.
- `dix_prod.h` — pure production QA helpers for band power, correlation, width, and crest math.
- `tests/test_pitch.c`, `tests/test_lufs.c`, `tests/test_prod.c` — unit tests for the DSP.
- `build.sh` — fetch headers, compile, sign, stage (macOS).
- `build-linux.sh` — compile and stage the Linux `.so`.
- `build-win.sh` — build the Windows `.dll` with MSYS2/MinGW.
- `vlc-3.0.23-include/` — VLC public headers (fetched by `build.sh`; git-ignored).

## Scope & honesty
A **real-time "watch it scroll" visualizer**, not a scrub-the-whole-file analyzer (that's Audacity's job). Loudness is now a real ITU-R BS.1770-4 / EBU R128 meter (`lufs` panel: momentary/short-term/gated-integrated), alongside the quick peak/RMS/crest/correlation meters.
A **real-time "watch it scroll" visualizer**, not a scrub-the-whole-file analyzer (that's Audacity's job). Loudness is a real ITU-R BS.1770-4 / EBU R128 meter (`lufs` panel: momentary/short-term/gated-integrated), and `PROD` adds playbook-derived production guidance from live analyzer metrics. It still does not do offline source separation, DAW/project inspection, true-peak oversampling, or ML chord/tempo transcription.

## Roadmap
See [BACKLOG.md](BACKLOG.md) — the initial backlog is cleared. Future ideas: onset/tempo/beat-grid detection, chord identification, and per-band stereo/mono-compatibility metering.
See [BACKLOG.md](BACKLOG.md). Future ideas: onset/tempo/beat-grid detection, chord identification, and sound-design diagnostics.

## License
Links VLC's `libvlccore` (LGPL-2.1+/GPL-2.0+), so this ships **GPL-2.0-or-later** to stay compatible. See [LICENSE](LICENSE).
Expand Down
118 changes: 118 additions & 0 deletions dix_prod.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/* dix_prod.h -- pure production-analysis helpers for the Dix Analyzer.
* These helpers stay VLC-independent so the playbook-derived mix QA math can be
* unit-tested without building the visualization plugin. */
#ifndef DIX_PROD_H
#define DIX_PROD_H

#include <math.h>

#define DIX_PROD_BAND_COUNT 8

enum dix_prod_band
{
DIX_PROD_SUB,
DIX_PROD_LOW,
DIX_PROD_MUD,
DIX_PROD_BODY,
DIX_PROD_PRESENCE,
DIX_PROD_HARSH,
DIX_PROD_AIR,
DIX_PROD_TOP
};

typedef struct
{
const char *label;
float lo_hz;
float hi_hz;
} dix_prod_band_t;

static const dix_prod_band_t DIX_PROD_BANDS[DIX_PROD_BAND_COUNT] =
{
{ "SUB", 20.f, 60.f },
{ "LOW", 60.f, 200.f },
{ "MUD", 200.f, 500.f },
{ "BODY", 500.f, 2000.f },
{ "PRES", 2000.f, 6000.f },
{ "HARSH",6000.f,10000.f },
{ "AIR", 10000.f,16000.f },
{ "TOP", 16000.f,20000.f }
};

static inline float dix_prod_clampf( float v, float lo, float hi )
{
if( v < lo ) return lo;
if( v > hi ) return hi;
return v;
}

static inline float dix_prod_rel_db( float numerator, float denominator )
{
return 10.f * log10f( ( numerator + 1e-20f ) / ( denominator + 1e-20f ) );
}

/* Mean squared FFT magnitude in a frequency range. Averaging by bin count keeps
* wide bands from looking louder only because they contain more bins. */
static inline float dix_prod_band_power( const float *lin_mag, int half_bins,
float sample_rate, int fft_size,
float lo_hz, float hi_hz )
{
if( lin_mag == 0 || half_bins <= 1 || sample_rate <= 0.f || fft_size <= 0 )
return 0.f;

int lo = (int)floorf( lo_hz * (float)fft_size / sample_rate );
int hi = (int)ceilf( hi_hz * (float)fft_size / sample_rate );
if( lo < 1 ) lo = 1;
if( hi >= half_bins ) hi = half_bins - 1;
if( hi < lo ) return 0.f;

double acc = 0.0;
int count = 0;
for( int i = lo; i <= hi; i++ )
{
double m = (double)lin_mag[i];
acc += m * m;
count++;
}
return count > 0 ? (float)( acc / (double)count ) : 0.f;
}

static inline float dix_prod_corr( double sum_lr, double sum_ll, double sum_rr )
{
if( sum_ll <= 1e-20 || sum_rr <= 1e-20 )
return 0.f;
float c = (float)( sum_lr / sqrt( sum_ll * sum_rr ) );
return dix_prod_clampf( c, -1.f, 1.f );
}

/* Side share of mid+side energy. 0 = mono center, 1 = all side. */
static inline float dix_prod_width( double mid_e, double side_e )
{
double total = mid_e + side_e;
if( total <= 1e-20 )
return 0.f;
return dix_prod_clampf( (float)sqrt( side_e / total ), 0.f, 1.f );
}

static inline float dix_prod_crest_db( float peak, float rms )
{
if( peak <= 1e-12f || rms <= 1e-12f )
return 0.f;
return 20.f * log10f( peak / rms );
}

/* Returns 0..1 for "inside target range"; falls off linearly to the wider warn
* range. Used for compact UI health bars, not hard pass/fail mastering. */
static inline float dix_prod_range_score( float v, float good_lo, float good_hi,
float warn_lo, float warn_hi )
{
if( v >= good_lo && v <= good_hi )
return 1.f;
if( v < warn_lo || v > warn_hi )
return 0.f;
if( v < good_lo )
return ( v - warn_lo ) / ( good_lo - warn_lo );
return ( warn_hi - v ) / ( warn_hi - good_hi );
}

#endif /* DIX_PROD_H */
53 changes: 53 additions & 0 deletions tests/test_prod.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/* Unit tests for pure production-analysis helpers (dix_prod.h).
* Build & run: cc -O2 -std=c99 -o test_prod tests/test_prod.c -lm && ./test_prod */
#include <math.h>
#include <stdio.h>

#include "../dix_prod.h"

static int fails = 0;

static void check( int ok, const char *msg )
{
if( ok )
printf( "ok - %s\n", msg );
else
{
printf( "FAIL - %s\n", msg );
fails++;
}
}

static int nearf_( float a, float b, float eps )
{
return fabsf( a - b ) <= eps;
}

int main( void )
{
float mag[512] = { 0.f };
for( int i = 20; i <= 60; i++ )
mag[i] = 2.f;
for( int i = 200; i <= 500; i++ )
mag[i] = 1.f;

float sub = dix_prod_band_power( mag, 512, 1024.f, 1024, 20.f, 60.f );
float mud = dix_prod_band_power( mag, 512, 1024.f, 1024, 200.f, 500.f );
check( nearf_( sub, 4.f, 0.0001f ), "band_power averages squared magnitudes" );
check( nearf_( mud, 1.f, 0.0001f ), "band_power isolates the requested band" );
check( nearf_( dix_prod_rel_db( sub, mud ), 6.0206f, 0.01f ), "rel_db returns 6.02 dB for 4x power" );

check( nearf_( dix_prod_corr( 4.0, 4.0, 4.0 ), 1.f, 0.0001f ), "corr identical channels = 1" );
check( nearf_( dix_prod_corr( -4.0, 4.0, 4.0 ), -1.f, 0.0001f ), "corr inverted channels = -1" );
check( nearf_( dix_prod_width( 9.0, 0.0 ), 0.f, 0.0001f ), "width mono center = 0" );
check( nearf_( dix_prod_width( 0.0, 9.0 ), 1.f, 0.0001f ), "width all side = 1" );
check( nearf_( dix_prod_width( 4.0, 4.0 ), 0.7071f, 0.001f ), "width equal mid side = sqrt half" );

check( nearf_( dix_prod_crest_db( 1.f, 0.25f ), 12.0412f, 0.01f ), "crest_db peak/rms" );
check( nearf_( dix_prod_range_score( 10.f, 8.f, 12.f, 5.f, 16.f ), 1.f, 0.0001f ),
"range_score inside target = 1" );
check( nearf_( dix_prod_range_score( 4.f, 8.f, 12.f, 5.f, 16.f ), 0.f, 0.0001f ),
"range_score outside warn = 0" );

return fails ? 1 : 0;
}
Loading
Loading