diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0c71b8..c4bf260 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/.gitignore b/.gitignore index bf52dc5..931194c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ vlc-3.0.23-include/ *.so *.o +# local private production-reference import +PRODUCTION_PLAYBOOK.md + .DS_Store diff --git a/BACKLOG.md b/BACKLOG.md index 8bfacce..8848d17 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -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. diff --git a/README.md b/README.md index bce29bd..da2a068 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) @@ -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 @@ -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. @@ -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). diff --git a/dix_prod.h b/dix_prod.h new file mode 100644 index 0000000..8ced1d6 --- /dev/null +++ b/dix_prod.h @@ -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 + +#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 */ diff --git a/tests/test_prod.c b/tests/test_prod.c new file mode 100644 index 0000000..9dfa94b --- /dev/null +++ b/tests/test_prod.c @@ -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 +#include + +#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; +} diff --git a/waveform.c b/waveform.c index 6475b8c..5d8ebcd 100644 --- a/waveform.c +++ b/waveform.c @@ -1,7 +1,8 @@ /* Dix Waveform - a pro-style audio analyzer visualization module for VLC 3.0.x. * v3: engineering dashboard (spectrogram, spectrum, stereo waveform, vectorscope, - * meters) plus a composition view (chromagram, key detection, note/cents, energy - * ribbon). Audio passes through untouched. */ + * meters), a composition view (chromagram, key detection, note/cents, energy + * ribbon), and a playbook-grounded production QA view. Audio passes through + * untouched. */ #ifdef HAVE_CONFIG_H # include "config.h" @@ -28,8 +29,9 @@ #include "dix_pitch.h" /* pure pitch/chroma/key DSP (unit-tested standalone) */ #include "dix_lufs.h" /* ITU-R BS.1770 loudness (unit-tested standalone) */ +#include "dix_prod.h" /* production QA helpers (unit-tested standalone) */ -#define DIX_VERSION "3.5.4" +#define DIX_VERSION "3.6.0" #define DIX_W 1280 #define DIX_H 720 #define DIX_MAX_FFT 8192 @@ -45,7 +47,8 @@ enum dix_layout DIX_LAYOUT_WAVEFORM, DIX_LAYOUT_SPECTRUM, DIX_LAYOUT_STEREO, - DIX_LAYOUT_COMPOSE + DIX_LAYOUT_COMPOSE, + DIX_LAYOUT_PRODUCTION }; enum dix_scale @@ -95,7 +98,12 @@ enum dix_panel DIX_PANEL_KEY, DIX_PANEL_NOTE, DIX_PANEL_ENERGY, - DIX_PANEL_LUFS + DIX_PANEL_LUFS, + DIX_PANEL_TONAL, + DIX_PANEL_LOW_MONO, + DIX_PANEL_WIDTH, + DIX_PANEL_CREST, + DIX_PANEL_MASK }; #define DIX_MAX_DASH 6 @@ -171,6 +179,22 @@ struct filter_sys_t float ribbon_hi[DIX_SPEC_W]; /* scrolling high-band energy history */ float ribbon_flux[DIX_SPEC_W]; /* scrolling spectral-flux (onset) history */ + /* Production QA metrics (PROD layout): playbook-derived mix/master checks. */ + float prod_band_db[DIX_PROD_BAND_COUNT]; /* band power vs. full-band avg, dB */ + float prod_low_corr; + float prod_mid_corr; + float prod_high_corr; + float prod_low_width; + float prod_mid_width; + float prod_high_width; + float prod_crest_db; + float prod_peak_db; + float prod_lp200_l; + float prod_lp200_r; + float prod_lp4k_l; + float prod_lp4k_r; + int prod_frames; + enum dix_layout layout; enum dix_scale scale; enum dix_channels channels; @@ -190,11 +214,11 @@ static int dix_text_w( const char *t, int scale ); /* fwd: used before its def static const char *const layout_values[] = { - "dashboard", "spectrogram", "waveform", "spectrum", "stereo", "compose" + "dashboard", "spectrogram", "waveform", "spectrum", "stereo", "compose", "production" }; static const char *const layout_text[] = { - "Dashboard", "Spectrogram", "Waveform", "Spectrum", "Stereo", "Compose" + "Dashboard", "Spectrogram", "Waveform", "Spectrum", "Stereo", "Compose", "Production" }; static const int fft_values[] = { 1024, 2048, 4096, 8192 }; static const char *const fft_text[] = { "1024", "2048", "4096", "8192" }; @@ -231,7 +255,7 @@ vlc_module_begin() set_section( "Dix Analyzer", NULL ) add_string( CFG_LAYOUT, "dashboard", "Analyzer layout", - "dashboard, spectrogram, waveform, spectrum, stereo, or compose", false ) + "dashboard, spectrogram, waveform, spectrum, stereo, compose, or production", false ) change_string_list( layout_values, layout_text ) add_integer( CFG_FFT, 4096, "FFT size", "1024, 2048, 4096, or 8192", false ) change_integer_list( fft_values, fft_text ) @@ -249,8 +273,8 @@ vlc_module_begin() change_string_list( reference_values, reference_text ) add_string( CFG_DASH, "", "Custom dashboard panels", "comma list auto-arranged into a grid, e.g. " - "\"spectrogram,spectrum,chroma,key\" " - "(panels: spectrogram, spectrum, waveform, stereo, chroma, key, note, energy, lufs). " + "\"spectrum,lufs,tonal,width\" " + "(panels: spectrogram, spectrum, waveform, stereo, chroma, key, note, energy, lufs, tonal, lowmono, width, crest, mask). " "Empty = default dashboard.", false ) add_string( CFG_TITLE, "", "Track title", "Optional title text drawn after the Dix Analyzer label", false ) @@ -560,7 +584,9 @@ static void parse_options( filter_t *f, filter_sys_t *s ) streq( v, "waveform" ) ? DIX_LAYOUT_WAVEFORM : streq( v, "spectrum" ) ? DIX_LAYOUT_SPECTRUM : streq( v, "stereo" ) ? DIX_LAYOUT_STEREO : - streq( v, "compose" ) ? DIX_LAYOUT_COMPOSE : DIX_LAYOUT_DASHBOARD; + streq( v, "compose" ) ? DIX_LAYOUT_COMPOSE : + ( streq( v, "production" ) || streq( v, "prod" ) ) ? DIX_LAYOUT_PRODUCTION : + DIX_LAYOUT_DASHBOARD; free( v ); int64_t fft = var_InheritInteger( f, CFG_FFT ); @@ -607,7 +633,12 @@ static void parse_options( filter_t *f, filter_sys_t *s ) streq( tok, "key" ) ? DIX_PANEL_KEY : streq( tok, "note" ) ? DIX_PANEL_NOTE : streq( tok, "energy" ) ? DIX_PANEL_ENERGY : - streq( tok, "lufs" ) ? DIX_PANEL_LUFS : -1; + streq( tok, "lufs" ) ? DIX_PANEL_LUFS : + streq( tok, "tonal" ) ? DIX_PANEL_TONAL : + ( streq( tok, "lowmono" ) || streq( tok, "mono" ) ) ? DIX_PANEL_LOW_MONO : + streq( tok, "width" ) ? DIX_PANEL_WIDTH : + streq( tok, "crest" ) ? DIX_PANEL_CREST : + streq( tok, "mask" ) ? DIX_PANEL_MASK : -1; if( p >= 0 ) s->dash_panels[s->dash_count++] = p; } @@ -809,6 +840,77 @@ static void update_meters( filter_sys_t *s, const float *samples, int n, unsigne s->clips_r = clips_r; } +static void update_production_stereo( filter_sys_t *s, const float *samples, int n, unsigned chans ) +{ + if( n <= 0 || chans == 0 ) + return; + + float fs = s->sample_rate > 0 ? (float)s->sample_rate : 48000.f; + float a200 = 1.f - expf( -2.f * DIX_PI * 200.f / fs ); + float a4k = 1.f - expf( -2.f * DIX_PI * 4000.f / fs ); + a200 = clamp_f( a200, 0.0001f, 1.f ); + a4k = clamp_f( a4k, 0.0001f, 1.f ); + + double lr[3] = { 0.0, 0.0, 0.0 }; + double ll[3] = { 0.0, 0.0, 0.0 }; + double rr[3] = { 0.0, 0.0, 0.0 }; + double mid_e[3] = { 0.0, 0.0, 0.0 }; + double side_e[3] = { 0.0, 0.0, 0.0 }; + + for( int i = 0; i < n; i++ ) + { + const float *src = samples + (size_t)i * chans; + float l = src[0]; + float r = chans > 1 ? src[1] : l; + + s->prod_lp200_l += a200 * ( l - s->prod_lp200_l ); + s->prod_lp200_r += a200 * ( r - s->prod_lp200_r ); + s->prod_lp4k_l += a4k * ( l - s->prod_lp4k_l ); + s->prod_lp4k_r += a4k * ( r - s->prod_lp4k_r ); + + float bl[3] = { + s->prod_lp200_l, + s->prod_lp4k_l - s->prod_lp200_l, + l - s->prod_lp4k_l + }; + float br[3] = { + s->prod_lp200_r, + s->prod_lp4k_r - s->prod_lp200_r, + r - s->prod_lp4k_r + }; + + for( int b = 0; b < 3; b++ ) + { + float m = ( bl[b] + br[b] ) * 0.5f; + float side = ( bl[b] - br[b] ) * 0.5f; + lr[b] += (double)bl[b] * br[b]; + ll[b] += (double)bl[b] * bl[b]; + rr[b] += (double)br[b] * br[b]; + mid_e[b] += (double)m * m; + side_e[b] += (double)side * side; + } + } + + float corr[3] = { + dix_prod_corr( lr[0], ll[0], rr[0] ), + dix_prod_corr( lr[1], ll[1], rr[1] ), + dix_prod_corr( lr[2], ll[2], rr[2] ) + }; + float width[3] = { + dix_prod_width( mid_e[0], side_e[0] ), + dix_prod_width( mid_e[1], side_e[1] ), + dix_prod_width( mid_e[2], side_e[2] ) + }; + + float a = s->prod_frames <= 0 ? 1.f : 0.22f; + s->prod_low_corr = s->prod_low_corr * ( 1.f - a ) + corr[0] * a; + s->prod_mid_corr = s->prod_mid_corr * ( 1.f - a ) + corr[1] * a; + s->prod_high_corr = s->prod_high_corr * ( 1.f - a ) + corr[2] * a; + s->prod_low_width = s->prod_low_width * ( 1.f - a ) + width[0] * a; + s->prod_mid_width = s->prod_mid_width * ( 1.f - a ) + width[1] * a; + s->prod_high_width = s->prod_high_width * ( 1.f - a ) + width[2] * a; +} + static int reverse_bits( int v, int bits ) { int r = 0; @@ -1065,6 +1167,35 @@ static void update_ribbon( filter_sys_t *s ) s->ribbon_flux[w - 1] = flux; } +static void update_production_spectrum( filter_sys_t *s ) +{ + int n = s->fft_size; + int half = n / 2; + float sr = s->sample_rate > 0 ? (float)s->sample_rate : 48000.f; + float full = dix_prod_band_power( s->lin_mag, half, sr, n, 20.f, 20000.f ); + if( full <= 1e-20f ) + full = 1e-20f; + + float a = s->prod_frames <= 0 ? 1.f : 0.15f; + for( int b = 0; b < DIX_PROD_BAND_COUNT; b++ ) + { + float p = dix_prod_band_power( s->lin_mag, half, sr, n, + DIX_PROD_BANDS[b].lo_hz, + DIX_PROD_BANDS[b].hi_hz ); + float rel = dix_prod_rel_db( p, full ); + rel = clamp_f( rel, -36.f, 36.f ); + s->prod_band_db[b] = s->prod_band_db[b] * ( 1.f - a ) + rel * a; + } + + float peak = s->peak_l > s->peak_r ? s->peak_l : s->peak_r; + float rms = ( s->rms_l + s->rms_r ) * 0.5f; + float crest = dix_prod_crest_db( peak, rms ); + float peak_db = peak > 1e-12f ? 20.f * log10f( peak ) : -90.f; + s->prod_crest_db = s->prod_crest_db * ( 1.f - a ) + crest * a; + s->prod_peak_db = s->prod_peak_db * ( 1.f - a ) + peak_db * a; + s->prod_frames++; +} + static void update_spectrogram( filter_sys_t *s ) { for( int y = 0; y < DIX_SPEC_H; y++ ) @@ -1487,8 +1618,181 @@ static void draw_energy_ribbon( filter_sys_t *s, rect_t r ) } } +static rgb_t prod_score_color( float score ) +{ + if( score >= 0.75f ) + return rgb( 112, 230, 150 ); + if( score >= 0.35f ) + return rgb( 255, 190, 96 ); + return rgb( 255, 86, 78 ); +} + +static void draw_prod_meter( filter_sys_t *s, rect_t r, const char *label, + float value, float lo, float hi, rgb_t c ) +{ + draw_text( s, r.x, r.y, label, rgb( 184, 200, 216 ), 2 ); + rect_t bar = { r.x + 104, r.y + 5, r.w - 176, 10 }; + fill_rect( s, bar, rgb( 7, 10, 16 ) ); + draw_rect( s, bar, rgb( 58, 70, 88 ) ); + float t = ( value - lo ) / ( hi - lo ); + t = clamp_f( t, 0.f, 1.f ); + int fill = (int)( t * (float)( bar.w - 2 ) ); + fill_rect( s, (rect_t){ bar.x + 1, bar.y + 1, fill, bar.h - 2 }, c ); + + char buf[32]; + snprintf( buf, sizeof( buf ), "%+.1FDB", value ); + draw_text( s, r.x + r.w - 64, r.y, buf, c, 2 ); +} + +static void draw_prod_ratio( filter_sys_t *s, rect_t r, const char *label, + float value, float lo, float hi, rgb_t c, + int percent ) +{ + draw_text( s, r.x, r.y, label, rgb( 184, 200, 216 ), 2 ); + rect_t bar = { r.x + 118, r.y + 5, r.w - 190, 10 }; + fill_rect( s, bar, rgb( 7, 10, 16 ) ); + draw_rect( s, bar, rgb( 58, 70, 88 ) ); + float t = ( value - lo ) / ( hi - lo ); + t = clamp_f( t, 0.f, 1.f ); + int fill = (int)( t * (float)( bar.w - 2 ) ); + fill_rect( s, (rect_t){ bar.x + 1, bar.y + 1, fill, bar.h - 2 }, c ); + + char buf[32]; + if( percent ) + snprintf( buf, sizeof( buf ), "%d%%", (int)lroundf( value * 100.f ) ); + else + snprintf( buf, sizeof( buf ), "%+.2F", value ); + draw_text( s, r.x + r.w - 58, r.y, buf, c, 2 ); +} + +static void draw_tonal_balance( filter_sys_t *s, rect_t r ) +{ + draw_panel( s, r, "TONAL BALANCE" ); + rect_t plot = { r.x + 16, r.y + 42, r.w - 32, r.h - 58 }; + int row_h = plot.h / DIX_PROD_BAND_COUNT; + int zero = plot.x + 110 + ( plot.w - 176 ) / 2; + draw_line( s, zero, plot.y, zero, plot.y + plot.h - 1, rgb( 78, 90, 108 ) ); + + for( int b = 0; b < DIX_PROD_BAND_COUNT; b++ ) + { + int y = plot.y + b * row_h + 3; + float v = s->prod_band_db[b]; + float mag = clamp_f( fabsf( v ) / 18.f, 0.f, 1.f ); + float score = dix_prod_range_score( v, -6.f, 6.f, -14.f, 14.f ); + rgb_t c = prod_score_color( score ); + int bw = (int)( mag * (float)( ( plot.w - 176 ) / 2 - 2 ) ); + draw_text( s, plot.x, y, DIX_PROD_BANDS[b].label, rgb( 184, 200, 216 ), 2 ); + fill_rect( s, (rect_t){ zero - ( plot.w - 176 ) / 2, y + 5, plot.w - 176, 8 }, rgb( 7, 10, 16 ) ); + if( v >= 0.f ) + fill_rect( s, (rect_t){ zero, y + 5, bw, 8 }, c ); + else + fill_rect( s, (rect_t){ zero - bw, y + 5, bw, 8 }, c ); + + char buf[32]; + snprintf( buf, sizeof( buf ), "%+.1FDB", v ); + draw_text( s, plot.x + plot.w - 64, y, buf, c, 2 ); + } + + draw_text( s, r.x + 16, r.y + r.h - 36, "PINK TILT CUT MUD WATCH HARSH", + rgb( 150, 168, 188 ), 2 ); + draw_text( s, r.x + 16, r.y + r.h - 18, "KEEP AIR CONTROLLED", + rgb( 150, 168, 188 ), 2 ); +} + +static void draw_low_mono( filter_sys_t *s, rect_t r ) +{ + draw_panel( s, r, "LOW MONO" ); + float corr_score = dix_prod_range_score( s->prod_low_corr, 0.75f, 1.f, 0.f, 1.f ); + float width_score = dix_prod_range_score( s->prod_low_width, 0.f, 0.18f, 0.f, 0.40f ); + draw_prod_ratio( s, (rect_t){ r.x + 18, r.y + 46, r.w - 36, 24 }, + "CORR", s->prod_low_corr, -1.f, 1.f, prod_score_color( corr_score ), 0 ); + draw_prod_ratio( s, (rect_t){ r.x + 18, r.y + 84, r.w - 36, 24 }, + "WIDTH", s->prod_low_width, 0.f, 0.6f, prod_score_color( width_score ), 1 ); + + const char *msg = ( s->prod_low_corr >= 0.75f && s->prod_low_width <= 0.18f ) + ? "LOW END MONO OK" : "CENTER KICK BASS SUB"; + rgb_t c = ( s->prod_low_corr >= 0.75f && s->prod_low_width <= 0.18f ) + ? rgb( 112, 230, 150 ) : rgb( 255, 190, 96 ); + draw_text_fit( s, r.x + 18, r.y + 132, r.w - 36, msg, c, 3 ); + draw_text( s, r.x + 18, r.y + r.h - 28, "SUB LOW MONO CORR POSITIVE", + rgb( 150, 168, 188 ), 2 ); +} + +static void draw_width_bands( filter_sys_t *s, rect_t r ) +{ + draw_panel( s, r, "WIDTH BY BAND" ); + const char *lab[3] = { "LOW", "MID", "HIGH" }; + float corr[3] = { s->prod_low_corr, s->prod_mid_corr, s->prod_high_corr }; + float width[3] = { s->prod_low_width, s->prod_mid_width, s->prod_high_width }; + + for( int i = 0; i < 3; i++ ) + { + int y = r.y + 48 + i * 62; + float corr_score = i == 0 ? dix_prod_range_score( corr[i], 0.75f, 1.f, 0.f, 1.f ) + : dix_prod_range_score( corr[i], 0.05f, 1.f, -0.25f, 1.f ); + rgb_t cc = prod_score_color( corr_score ); + draw_prod_ratio( s, (rect_t){ r.x + 18, y, r.w - 36, 24 }, + lab[i], corr[i], -1.f, 1.f, cc, 0 ); + draw_prod_ratio( s, (rect_t){ r.x + 18, y + 26, r.w - 36, 24 }, + "SIDE", width[i], 0.f, 0.8f, + i == 0 && width[i] > 0.25f ? rgb( 255, 86, 78 ) : rgb( 112, 190, 236 ), 1 ); + } + draw_text( s, r.x + 18, r.y + r.h - 48, "LOW NARROW HIGHS MAY WIDEN", + rgb( 150, 168, 188 ), 2 ); + draw_text( s, r.x + 18, r.y + r.h - 28, "CORR ABOVE ZERO", + rgb( 150, 168, 188 ), 2 ); +} + +static void draw_crest_health( filter_sys_t *s, rect_t r ) +{ + draw_panel( s, r, "CREST HEADROOM" ); + float crest_score = dix_prod_range_score( s->prod_crest_db, 8.f, 12.f, 5.f, 16.f ); + float peak_score = s->prod_peak_db <= -1.f ? 1.f : ( s->prod_peak_db <= -0.3f ? 0.5f : 0.f ); + draw_prod_meter( s, (rect_t){ r.x + 18, r.y + 46, r.w - 36, 24 }, + "CREST", s->prod_crest_db, 0.f, 20.f, prod_score_color( crest_score ) ); + draw_prod_meter( s, (rect_t){ r.x + 18, r.y + 86, r.w - 36, 24 }, + "PEAK", s->prod_peak_db, -36.f, 0.f, prod_score_color( peak_score ) ); + + float lu = dix_lufs_integrated( &s->lufs ); + char buf[48]; + if( lu <= DIX_LUFS_SILENT + 0.05f ) + snprintf( buf, sizeof( buf ), "LUFS I --" ); + else + snprintf( buf, sizeof( buf ), "LUFS I %+.1F", lu ); + draw_text( s, r.x + 18, r.y + 132, buf, rgb( 255, 200, 90 ), 2 ); + draw_text( s, r.x + 18, r.y + 160, "STREAM -14 CLUB -10 TO -9", + rgb( 126, 145, 164 ), 2 ); +} + +static void draw_mask_watch( filter_sys_t *s, rect_t r ) +{ + draw_panel( s, r, "MASK WATCH" ); + struct row { const char *label; int band; float good_lo; float good_hi; float warn_lo; float warn_hi; }; + static const struct row rows[] = { + { "MUD", DIX_PROD_MUD, -18.f, 4.f, -24.f, 12.f }, + { "VOCAL", DIX_PROD_PRESENCE, -8.f, 8.f, -18.f, 16.f }, + { "HARSH",DIX_PROD_HARSH, -24.f, 4.f, -30.f, 12.f }, + { "AIR", DIX_PROD_AIR, -18.f, 10.f, -30.f, 18.f } + }; + + for( int i = 0; i < 4; i++ ) + { + float v = s->prod_band_db[rows[i].band]; + float score = dix_prod_range_score( v, rows[i].good_lo, rows[i].good_hi, + rows[i].warn_lo, rows[i].warn_hi ); + draw_prod_meter( s, (rect_t){ r.x + 18, r.y + 44 + i * 44, r.w - 36, 24 }, + rows[i].label, v, -24.f, 18.f, prod_score_color( score ) ); + } + draw_text( s, r.x + 18, r.y + r.h - 68, "PAN FIRST MIRROR EQ", + rgb( 150, 168, 188 ), 2 ); + draw_text( s, r.x + 18, r.y + r.h - 48, "PROTECT VOCAL PRESENCE", + rgb( 150, 168, 188 ), 2 ); + draw_text( s, r.x + 18, r.y + r.h - 28, "CUT NARROW BOOST WIDE", + rgb( 150, 168, 188 ), 2 ); +} + /* ITU-R BS.1770 loudness: momentary / short-term / integrated with a scale - * and streaming (-14) / broadcast (-23) target ticks. */ + * and streaming (-14), club (-10..-9), and broadcast (-23) target ticks. */ static void draw_lufs( filter_sys_t *s, rect_t r ) { draw_panel( s, r, "LOUDNESS LUFS" ); @@ -1514,8 +1818,11 @@ static void draw_lufs( filter_sys_t *s, rect_t r ) fill_rect( s, (rect_t){ bx, by, bw, 3 }, rgb( 40, 48, 62 ) ); int gx = bx + (int)( ( -14.f - lo ) / ( hi - lo ) * bw ); int ex = bx + (int)( ( -23.f - lo ) / ( hi - lo ) * bw ); + int cx1 = bx + (int)( ( -10.f - lo ) / ( hi - lo ) * bw ); + int cx2 = bx + (int)( ( -9.f - lo ) / ( hi - lo ) * bw ); fill_rect( s, (rect_t){ gx, by - 6, 1, 15 }, rgb( 120, 200, 120 ) ); fill_rect( s, (rect_t){ ex, by - 6, 1, 15 }, rgb( 120, 150, 190 ) ); + fill_rect( s, (rect_t){ cx1, by - 8, cx2 - cx1 + 1, 19 }, rgb( 190, 124, 54 ) ); float mv = vals[2] > DIX_LUFS_SILENT + 0.05f ? vals[2] : ( vals[0] > DIX_LUFS_SILENT + 0.05f ? vals[0] : lo ); mv = clamp_f( mv, lo, hi ); @@ -1539,6 +1846,11 @@ static void draw_panel_token( filter_sys_t *s, int tok, rect_t r ) case DIX_PANEL_NOTE: draw_note_readout( s, r ); break; case DIX_PANEL_ENERGY: draw_energy_ribbon( s, r ); break; case DIX_PANEL_LUFS: draw_lufs( s, r ); break; + case DIX_PANEL_TONAL: draw_tonal_balance( s, r ); break; + case DIX_PANEL_LOW_MONO: draw_low_mono( s, r ); break; + case DIX_PANEL_WIDTH: draw_width_bands( s, r ); break; + case DIX_PANEL_CREST: draw_crest_health( s, r ); break; + case DIX_PANEL_MASK: draw_mask_watch( s, r ); break; } } @@ -1572,13 +1884,14 @@ static void draw_dash( filter_sys_t *s, rect_t area ) * handler (DixTabClick) and the renderer share dix_tab_rect(), so hit-testing * and drawing can never disagree. */ static const int dix_tab_layout[] = { - DIX_LAYOUT_DASHBOARD, DIX_LAYOUT_COMPOSE, DIX_LAYOUT_SPECTROGRAM, - DIX_LAYOUT_SPECTRUM, DIX_LAYOUT_WAVEFORM, DIX_LAYOUT_STEREO + DIX_LAYOUT_DASHBOARD, DIX_LAYOUT_PRODUCTION, DIX_LAYOUT_COMPOSE, + DIX_LAYOUT_SPECTROGRAM, DIX_LAYOUT_SPECTRUM, DIX_LAYOUT_WAVEFORM, + DIX_LAYOUT_STEREO }; static const char *const dix_tab_label[] = { - "DASH", "KEYS (COMPOSE)", "SPECTRO", "SPECTRUM", "WAVE", "STEREO" + "DASH", "PROD", "KEYS (COMPOSE)", "SPECTRO", "SPECTRUM", "WAVE", "STEREO" }; -#define DIX_TAB_COUNT 6 +#define DIX_TAB_COUNT 7 #define DIX_TAB_Y 8 #define DIX_TAB_H 28 #define DIX_TAB_X0 60 @@ -1628,6 +1941,12 @@ static rect_t dix_tab_rect( int i ) return r; } +static int dix_tabs_right( void ) +{ + rect_t last = dix_tab_rect( DIX_TAB_COUNT - 1 ); + return last.x + last.w; +} + static void draw_tab_bar( filter_sys_t *s ) { for( int i = 0; i < DIX_TAB_COUNT; i++ ) @@ -1655,13 +1974,15 @@ static void render_dashboard( filter_sys_t *s ) draw_tab_bar( s ); int hdr_right = DIX_W - 16; + int hdr_max = hdr_right - dix_tabs_right() - 18; + if( hdr_max < 0 ) hdr_max = 0; if( s->title[0] != '\0' ) - draw_text_right( s, hdr_right, 4, 470, s->title, rgb( 255, 198, 120 ), 2 ); + draw_text_right( s, hdr_right, 4, hdr_max, s->title, rgb( 255, 198, 120 ), 2 ); char status[80]; snprintf( status, sizeof( status ), "%uHZ FFT %d %s", s->sample_rate, s->fft_size, s->scale == DIX_SCALE_LOG ? "LOG" : "LINEAR" ); - draw_text_right( s, hdr_right, 22, 470, status, rgb( 158, 178, 198 ), 2 ); + draw_text_right( s, hdr_right, 22, hdr_max, status, rgb( 158, 178, 198 ), 2 ); if( s->layout == DIX_LAYOUT_SPECTROGRAM ) { @@ -1691,6 +2012,16 @@ static void render_dashboard( filter_sys_t *s ) draw_energy_ribbon( s, (rect_t){ 18, 452, DIX_W - 36, 250 } ); return; } + if( s->layout == DIX_LAYOUT_PRODUCTION ) + { + draw_tonal_balance( s, (rect_t){ 18, 42, 780, 328 } ); + draw_lufs( s, (rect_t){ 816, 42, 446, 156 } ); + draw_crest_health( s, (rect_t){ 816, 214, 446, 156 } ); + draw_low_mono( s, (rect_t){ 18, 388, 392, 314 } ); + draw_width_bands( s, (rect_t){ 426, 388, 392, 314 } ); + draw_mask_watch( s, (rect_t){ 834, 388, 428, 314 } ); + return; + } if( s->dash_count > 0 ) /* user-defined panel grid (--dix-dash) */ { @@ -1882,7 +2213,9 @@ static block_t *DoWork( filter_t *p_filter, block_t *p_in_buf ) append_history( p_sys, samples, n, chans ); append_wave_columns( p_sys, samples, n, chans ); update_meters( p_sys, samples, n, chans ); + update_production_stereo( p_sys, samples, n, chans ); compute_fft( p_sys ); + update_production_spectrum( p_sys ); update_spectrogram( p_sys ); /* Composition analysis (cheap; feeds the "compose" layout). Key is