diff --git a/README.md b/README.md index da2a068..0634221 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.6.0-orange)](https://github.com/CryptoJones/VibeComposing-Analyzer) +[![Version](https://img.shields.io/badge/version-3.6.1-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 diff --git a/dix_pitch.h b/dix_pitch.h index 895d8ef..0ef1fd1 100644 --- a/dix_pitch.h +++ b/dix_pitch.h @@ -11,10 +11,14 @@ #include #define DIX_A440 440.0f +#define DIX_PITCH_MAX_HARMONICS 6 static const char *const NOTE_NAMES[12] = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" }; +static const float DIX_PITCH_HARMONIC_WEIGHTS[DIX_PITCH_MAX_HARMONICS] = +{ 1.00f, 0.65f, 0.45f, 0.33f, 0.25f, 0.20f }; + /* Krumhansl-Kessler key profiles (major / natural minor tonal hierarchies). */ static const float DIX_KRUMHANSL_MAJ[12] = { 6.35f, 2.23f, 3.48f, 2.33f, 4.38f, 4.09f, 2.52f, 5.19f, 2.39f, 3.66f, 2.29f, 2.88f }; @@ -41,6 +45,124 @@ static inline float dix_cents_off( float hz ) return ( midi - (float)lroundf( midi ) ) * 100.f; } +static inline float dix_spectrum_mag_at_bin( const float *lin_mag, int half_bins, float bin ) +{ + if( lin_mag == 0 || half_bins <= 2 ) + return 0.f; + if( bin <= 1.f ) + return lin_mag[1]; + if( bin >= (float)( half_bins - 1 ) ) + return lin_mag[half_bins - 1]; + + int i = (int)floorf( bin ); + float frac = bin - (float)i; + return lin_mag[i] * ( 1.f - frac ) + lin_mag[i + 1] * frac; +} + +static inline float dix_spectrum_local_peak_bin( const float *lin_mag, int half_bins, + float center_bin ) +{ + if( lin_mag == 0 || half_bins <= 2 ) + return 0.f; + int i = (int)lroundf( center_bin ); + if( i < 1 ) i = 1; + if( i > half_bins - 2 ) i = half_bins - 2; + float a = lin_mag[i - 1], b = lin_mag[i], c = lin_mag[i + 1]; + float denom = a - 2.f * b + c; + float delta = denom != 0.f ? 0.5f * ( a - c ) / denom : 0.f; + if( delta < -0.5f ) delta = -0.5f; + if( delta > 0.5f ) delta = 0.5f; + return (float)i + delta; +} + +/* Harmonic pitch score for an FFT-bin candidate. Single pure tones still win at + * their actual bin, while a missing-fundamental stack can beat one loud overtone + * when several upper partials line up at integer multiples. */ +static inline float dix_harmonic_pitch_score( const float *lin_mag, int half_bins, + float candidate_bin ) +{ + if( lin_mag == 0 || half_bins <= 2 || candidate_bin < 1.f ) + return 0.f; + + float peak = 0.f; + for( int h = 1; h <= DIX_PITCH_MAX_HARMONICS; h++ ) + { + float hb = candidate_bin * (float)h; + if( hb >= (float)( half_bins - 1 ) ) + break; + float m = dix_spectrum_mag_at_bin( lin_mag, half_bins, hb ); + if( m > peak ) peak = m; + } + if( peak <= 1e-12f ) + return 0.f; + + float acc = 0.f; + float wsum = 0.f; + int hits = 0; + int fundamental_present = 0; + for( int h = 1; h <= DIX_PITCH_MAX_HARMONICS; h++ ) + { + float hb = candidate_bin * (float)h; + if( hb >= (float)( half_bins - 1 ) ) + break; + float w = DIX_PITCH_HARMONIC_WEIGHTS[h - 1]; + float m = dix_spectrum_mag_at_bin( lin_mag, half_bins, hb ); + acc += w * m; + wsum += w; + if( m >= peak * 0.08f ) + { + hits++; + if( h == 1 ) + fundamental_present = 1; + } + } + if( wsum <= 0.f || hits <= 0 ) + return 0.f; + float continuity = sqrtf( (float)hits ) * sqrtf( sqrtf( (float)hits ) ); + float score = ( acc / sqrtf( wsum ) ) * continuity; + if( !fundamental_present && hits < 3 ) + score *= 0.35f; + return score; +} + +static inline float dix_harmonic_pitch_refine_hz( const float *lin_mag, int half_bins, + int fft_size, float sample_rate, + float candidate_bin ) +{ + if( lin_mag == 0 || half_bins <= 2 || fft_size <= 0 || sample_rate <= 0.f ) + return 0.f; + + float peak = 0.f; + for( int h = 1; h <= DIX_PITCH_MAX_HARMONICS; h++ ) + { + float hb = candidate_bin * (float)h; + if( hb >= (float)( half_bins - 1 ) ) + break; + float pk = dix_spectrum_local_peak_bin( lin_mag, half_bins, hb ); + float m = dix_spectrum_mag_at_bin( lin_mag, half_bins, pk ); + if( m > peak ) peak = m; + } + if( peak <= 1e-12f ) + return candidate_bin * sample_rate / (float)fft_size; + + float hz_acc = 0.f; + float hz_w = 0.f; + for( int h = 1; h <= DIX_PITCH_MAX_HARMONICS; h++ ) + { + float hb = candidate_bin * (float)h; + if( hb >= (float)( half_bins - 1 ) ) + break; + float pk = dix_spectrum_local_peak_bin( lin_mag, half_bins, hb ); + float mag = dix_spectrum_mag_at_bin( lin_mag, half_bins, pk ); + if( mag < peak * 0.08f ) + continue; + float w = DIX_PITCH_HARMONIC_WEIGHTS[h - 1] * mag; + hz_acc += ( pk * sample_rate / (float)fft_size ) / (float)h * w; + hz_w += w; + } + return hz_w > 1e-12f ? hz_acc / hz_w : candidate_bin * sample_rate / (float)fft_size; +} + /* Centered (Pearson) correlation of two 12-length vectors. */ static inline float dix_corr12( const float *a, const float *b ) { diff --git a/dix_prod.h b/dix_prod.h index 8ced1d6..3e6c191 100644 --- a/dix_prod.h +++ b/dix_prod.h @@ -7,6 +7,7 @@ #include #define DIX_PROD_BAND_COUNT 8 +#define DIX_PROD_PI 3.14159265358979323846f enum dix_prod_band { @@ -27,6 +28,16 @@ typedef struct float hi_hz; } dix_prod_band_t; +typedef struct +{ + float b0, b1, b2, a1, a2; +} dix_prod_biquad_t; + +typedef struct +{ + float x1, x2, y1, y2; +} dix_prod_biquad_state_t; + static const dix_prod_band_t DIX_PROD_BANDS[DIX_PROD_BAND_COUNT] = { { "SUB", 20.f, 60.f }, @@ -51,30 +62,138 @@ 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 ) +static inline dix_prod_biquad_t dix_prod_lowpass2( float sample_rate, + float cutoff_hz, float q ) { - if( lin_mag == 0 || half_bins <= 1 || sample_rate <= 0.f || fft_size <= 0 ) + dix_prod_biquad_t c = { 1.f, 0.f, 0.f, 0.f, 0.f }; + if( sample_rate <= 0.f || cutoff_hz <= 0.f || q <= 0.f ) + return c; + if( cutoff_hz > sample_rate * 0.45f ) + cutoff_hz = sample_rate * 0.45f; + + float w0 = 2.f * DIX_PROD_PI * cutoff_hz / sample_rate; + float cw = cosf( w0 ); + float alpha = sinf( w0 ) / ( 2.f * q ); + float a0 = 1.f + alpha; + c.b0 = ( 1.f - cw ) * 0.5f / a0; + c.b1 = ( 1.f - cw ) / a0; + c.b2 = c.b0; + c.a1 = ( -2.f * cw ) / a0; + c.a2 = ( 1.f - alpha ) / a0; + return c; +} + +static inline float dix_prod_biquad_process( const dix_prod_biquad_t *c, + dix_prod_biquad_state_t *s, + float x ) +{ + float y = c->b0 * x + c->b1 * s->x1 + c->b2 * s->x2 + - c->a1 * s->y1 - c->a2 * s->y2; + s->x2 = s->x1; s->x1 = x; + s->y2 = s->y1; s->y1 = y; + return y; +} + +/* Cheap 4x intersample peak estimate using Catmull-Rom cubic interpolation over + * one segment y0..y1 with neighbor samples ym1/y2. It is not a standards-grade + * true-peak meter, but it catches many intersample overs better than sample peak. */ +static inline float dix_prod_interp_peak4( float ym1, float y0, float y1, float y2 ) +{ + float peak = fabsf( y0 ); + float ay1 = fabsf( y1 ); + if( ay1 > peak ) peak = ay1; + for( int i = 1; i < 4; i++ ) + { + float t = (float)i * 0.25f; + float t2 = t * t; + float t3 = t2 * t; + float y = 0.5f * ( 2.f * y0 + ( -ym1 + y1 ) * t + + ( 2.f * ym1 - 5.f * y0 + 4.f * y1 - y2 ) * t2 + + ( -ym1 + 3.f * y0 - 3.f * y1 + y2 ) * t3 ); + float ay = fabsf( y ); + if( ay > peak ) peak = ay; + } + return peak; +} + +/* FFT-bin power integrated over a frequency range with fractional edge bins. + * Treat each bin as covering center +/- half a bin so low bands do not jump by + * one whole bin when a boundary lands between centers. */ +static inline float dix_prod_band_energy( 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 || + hi_hz <= lo_hz ) 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 ); + float df = sample_rate / (float)fft_size; + float nyq_edge = ( (float)half_bins - 0.5f ) * df; + if( lo_hz < 0.f ) lo_hz = 0.f; + if( hi_hz > nyq_edge ) hi_hz = nyq_edge; + if( hi_hz <= lo_hz ) return 0.f; + + double acc = 0.0; + int lo = (int)floorf( lo_hz / df - 0.5f ); + int hi = (int)ceilf( hi_hz / df + 0.5f ); 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++ ) { + float bin_lo = ( (float)i - 0.5f ) * df; + float bin_hi = ( (float)i + 0.5f ) * df; + float ov_lo = bin_lo > lo_hz ? bin_lo : lo_hz; + float ov_hi = bin_hi < hi_hz ? bin_hi : hi_hz; + float weight = ( ov_hi - ov_lo ) / df; + if( weight <= 0.f ) continue; double m = (double)lin_mag[i]; - acc += m * m; - count++; + acc += m * m * (double)weight; } - return count > 0 ? (float)( acc / (double)count ) : 0.f; + return (float)acc; +} + +/* Mean squared FFT magnitude in a frequency range. Averaging by effective bin + * count keeps wide bands from looking louder only because they contain more + * bins; fractional edges keep narrow low-frequency ranges stable. */ +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 || + hi_hz <= lo_hz ) + return 0.f; + + float df = sample_rate / (float)fft_size; + float nyq_edge = ( (float)half_bins - 0.5f ) * df; + float clipped_lo = lo_hz < 0.f ? 0.f : lo_hz; + float clipped_hi = hi_hz > nyq_edge ? nyq_edge : hi_hz; + if( clipped_hi <= clipped_lo ) return 0.f; + + float bins = ( clipped_hi - clipped_lo ) / df; + if( bins <= 1e-9f ) return 0.f; + return dix_prod_band_energy( lin_mag, half_bins, sample_rate, fft_size, + clipped_lo, clipped_hi ) / bins; +} + +/* Energy per octave. This is the useful comparator for production balance + * because the playbook's pink-noise target is roughly equal energy per octave, + * not equal energy per linear-Hz bin. */ +static inline float dix_prod_band_octave_density( 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; + float df = sample_rate / (float)fft_size; + float nyq_edge = ( (float)half_bins - 0.5f ) * df; + if( lo_hz < 1.f ) lo_hz = 1.f; + if( hi_hz > nyq_edge ) hi_hz = nyq_edge; + if( hi_hz <= lo_hz ) return 0.f; + float octaves = log2f( hi_hz / lo_hz ); + if( octaves <= 1e-9f ) return 0.f; + return dix_prod_band_energy( lin_mag, half_bins, sample_rate, fft_size, + lo_hz, hi_hz ) / octaves; } static inline float dix_prod_corr( double sum_lr, double sum_ll, double sum_rr ) diff --git a/tests/test_pitch.c b/tests/test_pitch.c index 47b5c04..9c20a28 100644 --- a/tests/test_pitch.c +++ b/tests/test_pitch.c @@ -39,6 +39,26 @@ int main( void ) "cents_off(444) ~ +15.7 (sharp)" ); check( dix_cents_off( 437.f ) < 0.f, "cents_off(437) is flat (negative)" ); + /* Harmonic pitch scoring */ + float spec[4096] = { 0.f }; + spec[440] = 1.f; + check( dix_harmonic_pitch_score( spec, 4096, 440.f ) > + dix_harmonic_pitch_score( spec, 4096, 220.f ), + "harmonic_score pure sine stays at its actual bin" ); + check( nearf( dix_harmonic_pitch_refine_hz( spec, 4096, 8192, 8192.f, 440.f ), + 440.f, 0.01f ), + "harmonic_refine returns pure sine frequency" ); + + memset( spec, 0, sizeof( spec ) ); + spec[880] = 1.0f; spec[1320] = 0.9f; spec[1760] = 0.8f; + spec[2200] = 0.6f; spec[2640] = 0.5f; + check( dix_harmonic_pitch_score( spec, 4096, 440.f ) > + dix_harmonic_pitch_score( spec, 4096, 880.f ), + "harmonic_score can recover a missing fundamental from aligned partials" ); + check( nearf( dix_harmonic_pitch_refine_hz( spec, 4096, 8192, 8192.f, 440.f ), + 440.f, 0.01f ), + "harmonic_refine estimates missing fundamental frequency" ); + /* 12-vector correlation */ float a[12], b[12]; for( int i = 0; i < 12; i++ ) { a[i] = (float)i; b[i] = (float)i; } diff --git a/tests/test_prod.c b/tests/test_prod.c index 9dfa94b..1f3da4e 100644 --- a/tests/test_prod.c +++ b/tests/test_prod.c @@ -37,6 +37,33 @@ int main( void ) 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" ); + dix_prod_biquad_t lp = dix_prod_lowpass2( 48000.f, 200.f, 0.70710678f ); + float dc_gain = ( lp.b0 + lp.b1 + lp.b2 ) / ( 1.f + lp.a1 + lp.a2 ); + check( nearf_( dc_gain, 1.f, 0.001f ), "lowpass2 has unity DC gain" ); + dix_prod_biquad_state_t st = { 0.f, 0.f, 0.f, 0.f }; + float y = 0.f; + for( int i = 0; i < 4000; i++ ) + y = dix_prod_biquad_process( &lp, &st, 1.f ); + check( y > 0.99f && y < 1.01f, "lowpass2 passes steady DC" ); + + check( nearf_( dix_prod_interp_peak4( 0.5f, 0.5f, 0.5f, 0.5f ), 0.5f, 0.0001f ), + "interp_peak4 keeps flat signal peak" ); + check( dix_prod_interp_peak4( 0.f, 0.8660254f, 0.8660254f, 0.f ) > 0.90f, + "interp_peak4 catches between-sample peak" ); + + float edge[512] = { 0.f }; + edge[10] = 2.f; + check( nearf_( dix_prod_band_power( edge, 512, 1024.f, 1024, 9.75f, 10.25f ), 4.f, 0.0001f ), + "band_power handles fractional bin edges" ); + + float pinkish[512] = { 0.f }; + for( int i = 1; i < 512; i++ ) + pinkish[i] = 1.f / sqrtf( (float)i ); + float oct1 = dix_prod_band_octave_density( pinkish, 512, 1024.f, 1024, 20.f, 40.f ); + float oct2 = dix_prod_band_octave_density( pinkish, 512, 1024.f, 1024, 40.f, 80.f ); + check( fabsf( dix_prod_rel_db( oct1, oct2 ) ) < 0.25f, + "octave_density treats pink-like energy as balanced across octaves" ); + 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" ); diff --git a/waveform.c b/waveform.c index 5d8ebcd..c26646c 100644 --- a/waveform.c +++ b/waveform.c @@ -31,7 +31,7 @@ #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.6.0" +#define DIX_VERSION "3.6.1" #define DIX_W 1280 #define DIX_H 720 #define DIX_MAX_FFT 8192 @@ -189,10 +189,10 @@ struct filter_sys_t 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; + dix_prod_biquad_state_t prod_lp200_l; + dix_prod_biquad_state_t prod_lp200_r; + dix_prod_biquad_state_t prod_lp4k_l; + dix_prod_biquad_state_t prod_lp4k_r; int prod_frames; enum dix_layout layout; @@ -811,8 +811,17 @@ static void update_meters( filter_sys_t *s, const float *samples, int n, unsigne float al = fabsf( l ); float ar = fabsf( r ); - if( al > peak_l ) peak_l = al; - if( ar > peak_r ) peak_r = ar; + float nl = i + 1 < n ? samples[(size_t)( i + 1 ) * chans] : l; + float nr = i + 1 < n ? ( chans > 1 ? samples[(size_t)( i + 1 ) * chans + 1] : nl ) : r; + float pl = i > 0 ? samples[(size_t)( i - 1 ) * chans] : l; + float pr = i > 0 ? ( chans > 1 ? samples[(size_t)( i - 1 ) * chans + 1] : pl ) : r; + float n2l = i + 2 < n ? samples[(size_t)( i + 2 ) * chans] : nl; + float n2r = i + 2 < n ? ( chans > 1 ? samples[(size_t)( i + 2 ) * chans + 1] : n2l ) : nr; + float il = dix_prod_interp_peak4( pl, l, nl, n2l ); + float ir = dix_prod_interp_peak4( pr, r, nr, n2r ); + + if( il > peak_l ) peak_l = il; + if( ir > peak_r ) peak_r = ir; if( al >= 0.999f ) clips_l++; if( ar >= 0.999f ) clips_r++; @@ -846,10 +855,8 @@ static void update_production_stereo( filter_sys_t *s, const float *samples, int 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 ); + dix_prod_biquad_t lp200 = dix_prod_lowpass2( fs, 200.f, 0.70710678f ); + dix_prod_biquad_t lp4k = dix_prod_lowpass2( fs, 4000.f, 0.70710678f ); double lr[3] = { 0.0, 0.0, 0.0 }; double ll[3] = { 0.0, 0.0, 0.0 }; @@ -863,20 +870,20 @@ static void update_production_stereo( filter_sys_t *s, const float *samples, int 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 l200 = dix_prod_biquad_process( &lp200, &s->prod_lp200_l, l ); + float r200 = dix_prod_biquad_process( &lp200, &s->prod_lp200_r, r ); + float l4k = dix_prod_biquad_process( &lp4k, &s->prod_lp4k_l, l ); + float r4k = dix_prod_biquad_process( &lp4k, &s->prod_lp4k_r, r ); float bl[3] = { - s->prod_lp200_l, - s->prod_lp4k_l - s->prod_lp200_l, - l - s->prod_lp4k_l + l200, + l4k - l200, + l - l4k }; float br[3] = { - s->prod_lp200_r, - s->prod_lp4k_r - s->prod_lp200_r, - r - s->prod_lp4k_r + r200, + r4k - r200, + r - r4k }; for( int b = 0; b < 3; b++ ) @@ -1055,7 +1062,15 @@ static int freq_to_bin( const filter_sys_t *s, float t ) #define DIX_MUS_LO_HZ 60.f #define DIX_MUS_HI_HZ 2100.f -/* Fold linear-magnitude bins into 12 octave-collapsed pitch classes. */ +static int midi_to_pc( int midi ) +{ + int pc = midi % 12; + return pc < 0 ? pc + 12 : pc; +} + +/* Fold FFT-bin power into 12 octave-collapsed pitch classes. Bins are split + * between adjacent semitone centers, which makes chroma/key steadier when a + * partial falls between FFT bins or between equal-tempered note centers. */ static void compute_chroma( filter_sys_t *s ) { float raw[12] = { 0.f }; @@ -1067,22 +1082,29 @@ static void compute_chroma( filter_sys_t *s ) float hz = (float)i * sr / (float)n; if( hz < DIX_MUS_LO_HZ || hz > DIX_MUS_HI_HZ ) continue; - int pc = dix_pitch_class( dix_hz_to_midi( hz ) ); - raw[pc] += s->lin_mag[i]; + float midi = dix_hz_to_midi( hz ); + int lo_note = (int)floorf( midi ); + float frac = midi - (float)lo_note; + float m = s->lin_mag[i]; + float power = m * m; + raw[midi_to_pc( lo_note )] += power * ( 1.f - frac ); + raw[midi_to_pc( lo_note + 1 )] += power * frac; } float mx = 1e-9f; for( int p = 0; p < 12; p++ ) if( raw[p] > mx ) mx = raw[p]; for( int p = 0; p < 12; p++ ) { - float v = raw[p] / mx; + float v = sqrtf( raw[p] / mx ); s->chroma[p] = v; s->chroma_smooth[p] = s->chroma_smooth[p] * 0.85f + v * 0.15f; } } -/* Nearest-note + cents of the strongest partial in the musical range, with - * parabolic peak interpolation (Mathematics and Music §4.5) for sub-bin accuracy. */ +/* Nearest-note + cents of the strongest harmonic pitch candidate. The old + * peak-bin method confused loud overtones/formants for fundamentals; this + * scores each candidate by a short harmonic stack, then estimates f0 from the + * local peaks of the harmonics that are actually present. */ static void detect_dominant_pitch( filter_sys_t *s ) { int n = s->fft_size; @@ -1094,22 +1116,24 @@ static void detect_dominant_pitch( filter_sys_t *s ) if( hi > half - 2 ) hi = half - 2; int best = -1; - float bestm = 0.f; + float best_score = 0.f; for( int i = lo; i <= hi; i++ ) - if( s->lin_mag[i] > bestm ) { bestm = s->lin_mag[i]; best = i; } + { + float score = dix_harmonic_pitch_score( s->lin_mag, half, (float)i ); + if( score > best_score ) + { + best_score = score; + best = i; + } + } - if( best < 1 || bestm < 1e-6f ) + if( best < 1 || best_score < 1e-6f ) { s->dom_hz = 0.f; /* effectively silent — no pitch */ return; } - float a = s->lin_mag[best - 1], b = s->lin_mag[best], c = s->lin_mag[best + 1]; - float denom = a - 2.f * b + c; - float delta = denom != 0.f ? 0.5f * ( a - c ) / denom : 0.f; - delta = clamp_f( delta, -0.5f, 0.5f ); - - float hz = ( (float)best + delta ) * sr / (float)n; + float hz = dix_harmonic_pitch_refine_hz( s->lin_mag, half, n, sr, (float)best ); float midi = dix_hz_to_midi( hz ); int note = (int)lroundf( midi ); s->dom_hz = hz; @@ -1132,7 +1156,7 @@ static void estimate_key( filter_sys_t *s ) s->key_score[k] = s->key_score[k] * 0.9f + sc[k] * 0.1f; if( s->key_score[k] > bm ) { bm = s->key_score[k]; best = k; } } - s->key_index = best; + s->key_index = bm >= 0.12f ? best : -1; s->key_conf = bm; } @@ -1172,16 +1196,19 @@ 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 ); + float full_hi = sr * 0.5f; + if( full_hi > 20000.f ) + full_hi = 20000.f; + float full = dix_prod_band_octave_density( s->lin_mag, half, sr, n, 20.f, full_hi ); 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 p = dix_prod_band_octave_density( 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; @@ -1883,7 +1910,7 @@ static void draw_dash( filter_sys_t *s, rect_t area ) * with a mouse click inside the Dix window -- no VLC menu diving. The click * handler (DixTabClick) and the renderer share dix_tab_rect(), so hit-testing * and drawing can never disagree. */ -static const int dix_tab_layout[] = { +static const enum dix_layout dix_tab_layout[] = { DIX_LAYOUT_DASHBOARD, DIX_LAYOUT_PRODUCTION, DIX_LAYOUT_COMPOSE, DIX_LAYOUT_SPECTROGRAM, DIX_LAYOUT_SPECTRUM, DIX_LAYOUT_WAVEFORM, DIX_LAYOUT_STEREO