@@ -426,40 +426,89 @@ export function plotFir (h, title, opts = {}) {
426426}
427427
428428/**
429- * Generate 4-panel SVG comparing multiple SOS filters overlaid.
430- * @param {Array } filters - Array of [name, sos] or [name, sos, color]
429+ * Generate 4-panel SVG comparing multiple SOS filters or FIR impulse responses overlaid.
430+ * @param {Array } filters - Array of [name, sos|fir] or [name, sos|fir, color]
431+ * sos: [{b0,b1,b2,a1,a2}, ...] — IIR biquad cascade
432+ * fir: Float64Array|number[] — impulse response
431433 * @param {string } [title] - Plot title
432- * @param {object } [opts] - Options: { fs, bins, irLength }
434+ * @param {object } [opts] - Options: { fs, bins, irLength, irMax, gdMin, gdMax }
433435 * @returns {string } SVG markup
434436 */
435437export function plotCompare ( filters , title , opts = { } ) {
436438 let fs = opts . fs || theme . fs , NF = opts . bins || theme . bins
437- let irLen = opts . irLength || 128
439+
440+ function isFir ( data ) {
441+ return ! ( Array . isArray ( data ) && data . length > 0 && typeof data [ 0 ] === 'object' && 'b0' in data [ 0 ] )
442+ }
443+
444+ function firFreqz ( h ) {
445+ let frequencies = new Float64Array ( NF ) , magnitude = new Float64Array ( NF ) , phase = new Float64Array ( NF )
446+ for ( let k = 0 ; k < NF ; k ++ ) {
447+ frequencies [ k ] = k * fs / ( 2 * NF )
448+ let re = 0 , im = 0 , w = Math . PI * k / NF
449+ for ( let n = 0 ; n < h . length ; n ++ ) { re += h [ n ] * Math . cos ( w * n ) ; im -= h [ n ] * Math . sin ( w * n ) }
450+ magnitude [ k ] = Math . sqrt ( re * re + im * im )
451+ phase [ k ] = Math . atan2 ( im , re )
452+ }
453+ let delay = new Float64Array ( NF )
454+ let phDeg = Array . from ( phase ) . map ( v => v * 180 / Math . PI )
455+ for ( let k = 1 ; k < NF - 1 ; k ++ ) {
456+ let dp = phDeg [ k + 1 ] - phDeg [ k - 1 ]
457+ if ( dp > 180 ) dp -= 360 ; if ( dp < - 180 ) dp += 360
458+ delay [ k ] = - dp / ( 2 * Math . PI / NF * 180 / Math . PI )
459+ }
460+ delay [ 0 ] = delay [ 1 ]
461+ return { frequencies, magnitude, phase, delay }
462+ }
463+
464+ // Pre-compute all responses for range detection
465+ let defaultIrLen = opts . irLength || 128
466+ let computed = filters . map ( ( [ , data ] ) => {
467+ if ( isFir ( data ) ) {
468+ let r = firFreqz ( data )
469+ return { r, ir : data , gdFreqs : r . frequencies , gdDelay : r . delay }
470+ }
471+ let r = freqz ( data , NF , fs )
472+ let gd = groupDelay ( data , NF , fs )
473+ return { r, ir : impulseResponse ( data , defaultIrLen ) , gdFreqs : gd . frequencies , gdDelay : gd . delay }
474+ } )
475+
476+ // Auto-range: irMax and irLen from actual data
477+ let irLen = opts . irLength ?? Math . max ( ...computed . map ( ( { ir } ) => ir . length ) )
478+ let irMax = opts . irMax ?? 0
479+ if ( ! opts . irMax ) {
480+ for ( let { ir } of computed ) for ( let i = 0 ; i < ir . length ; i ++ ) { let v = Math . abs ( ir [ i ] ) ; if ( v > irMax ) irMax = v }
481+ irMax = Math . max ( irMax * 1.15 , 1e-10 )
482+ }
483+
484+ // Auto-range: group delay from actual data (unless overridden)
485+ let gdLo = opts . gdMin ?? null , gdHi = opts . gdMax ?? null
486+ if ( gdLo == null || gdHi == null ) {
487+ let allGd = [ ]
488+ for ( let { gdDelay } of computed ) for ( let v of gdDelay ) if ( isFinite ( v ) ) allGd . push ( v )
489+ let [ lo , hi ] = allGd . length ? robustRange ( allGd ) : [ - 25 , 5 ]
490+ if ( gdLo == null ) gdLo = lo
491+ if ( gdHi == null ) gdHi = hi
492+ }
438493
439494 let s = svgOpen ( )
440495 if ( title ) s += ` <text x="${ P2 . x + P2 . w } " y="${ P2 . y - 5 } " text-anchor="end" font-size="11" font-weight="600" fill="${ theme . text } ">${ title } </text>\n`
441496
442497 let fc = parseFc ( title )
443498 s += panel ( P1 , 'Hz' , 'dB' , - 80 , 20 , 0 ) + logXTicks ( P1 , fTicks , 10 , 20000 ) + dbGrid ( P1 ) + fcLine ( P1 , fc )
444499 s += panel ( P2 , 'Hz' , 'Phase (deg)' , - 180 , 180 , 0 ) + logXTicks ( P2 , fTicks , 10 , 20000 ) + phaseGrid ( P2 ) + fcLine ( P2 , fc )
445-
446- let gdLo = opts . gdMin ?? - 25 , gdHi = opts . gdMax ?? 5
447500 s += panel ( P3 , 'Hz' , 'Group delay' , gdLo , gdHi , 0 ) + logXTicks ( P3 , fTicks , 10 , 20000 ) + hTicks ( P3 , autoTicks ( gdLo , gdHi , 4 ) , gdLo , gdHi ) + fcLine ( P3 , fc )
448-
449- let irMax = opts . irMax ?? 0.35
450501 s += panel ( P4 , 'Samples' , 'Impulse' , - irMax , irMax , 0 ) + linXTicks ( P4 , autoTicks ( 0 , irLen , 3 ) . map ( Math . round ) , 0 , irLen ) + hTicks ( P4 , autoTicks ( - irMax , irMax , 3 ) , - irMax , irMax )
451502
452503 for ( let i = 0 ; i < filters . length ; i ++ ) {
453- let [ name , sos , c ] = filters [ i ]
504+ let [ , , c ] = filters [ i ]
454505 c = c || theme . colors [ i % theme . colors . length ]
455- let r = freqz ( sos , NF , fs )
506+ let { r , ir , gdFreqs , gdDelay } = computed [ i ]
456507 let db = Array . from ( mag2db ( r . magnitude ) )
457508 let phase = Array . from ( r . phase ) . map ( v => v * 180 / Math . PI )
458- let gd = groupDelay ( sos , NF , fs )
459- let ir = impulseResponse ( sos , irLen )
460509 s += logPoly ( P1 , r . frequencies , db , 10 , 20000 , - 80 , 20 , c , 1.3 , false )
461510 s += logPoly ( P2 , r . frequencies , phase , 10 , 20000 , - 180 , 180 , c , 1.3 , false )
462- s += logPoly ( P3 , gd . frequencies , Array . from ( gd . delay ) , 10 , 20000 , gdLo , gdHi , c , 1.3 , false )
511+ s += logPoly ( P3 , gdFreqs , Array . from ( gdDelay ) , 10 , 20000 , gdLo , gdHi , c , 1.3 , false )
463512 s += linPoly ( P4 , ir , 0 , irLen , - irMax , irMax , c )
464513 }
465514
0 commit comments