@@ -65,7 +65,7 @@ export function SweepFrPage({
6565
6666 const currentNorm = Math . min ( 1 , Math . max ( 0 , ( monitor . currentDbfs + 96 ) / 96 ) ) ;
6767 const peakNorm = Math . min ( 1 , Math . max ( 0 , ( monitor . peakDbfs + 96 ) / 96 ) ) ;
68- const roughFrPath = ( ( ) => {
68+ const roughFrGraph = ( ( ) => {
6969 const freqs = monitor . roughFrHz ;
7070 const values = monitor . roughFrDb ;
7171 if ( freqs . length < 2 || values . length < 2 || freqs . length !== values . length ) {
@@ -77,15 +77,58 @@ export function SweepFrPage({
7777 const maxLog = Math . log10 ( maxHz ) ;
7878 const spanLog = Math . max ( 1e-6 , maxLog - minLog ) ;
7979
80- return values
81- . map ( ( value , index ) => {
82- const hz = Math . min ( maxHz , Math . max ( minHz , freqs [ index ] ) ) ;
83- const x = ( ( Math . log10 ( hz ) - minLog ) / spanLog ) * 100 ;
84- const clamped = Math . max ( - 18 , Math . min ( 18 , value ) ) ;
85- const y = ( ( 18 - clamped ) / 36 ) * 100 ;
86- return `${ x . toFixed ( 2 ) } ,${ y . toFixed ( 2 ) } ` ;
80+ const smoothValues = values . map ( ( _ , index ) => {
81+ let weightedSum = 0 ;
82+ let weightTotal = 0 ;
83+ for ( let offset = - 2 ; offset <= 2 ; offset += 1 ) {
84+ const target = index + offset ;
85+ if ( target < 0 || target >= values . length ) {
86+ continue ;
87+ }
88+ const weight = offset === 0 ? 3 : Math . abs ( offset ) === 1 ? 2 : 1 ;
89+ weightedSum += values [ target ] * weight ;
90+ weightTotal += weight ;
91+ }
92+ return weightTotal > 0 ? weightedSum / weightTotal : values [ index ] ;
93+ } ) ;
94+
95+ const points = smoothValues . map ( ( value , index ) => {
96+ const hz = Math . min ( maxHz , Math . max ( minHz , freqs [ index ] ) ) ;
97+ const x = ( ( Math . log10 ( hz ) - minLog ) / spanLog ) * 100 ;
98+ const clamped = Math . max ( - 18 , Math . min ( 18 , value ) ) ;
99+ const y = 10 + ( ( 18 - clamped ) / 36 ) * 80 ;
100+ return { x, y } ;
101+ } ) ;
102+
103+ if ( points . length < 2 ) {
104+ return null ;
105+ }
106+
107+ let linePath = `M ${ points [ 0 ] . x . toFixed ( 2 ) } ${ points [ 0 ] . y . toFixed ( 2 ) } ` ;
108+ for ( let i = 1 ; i < points . length - 1 ; i += 1 ) {
109+ const midX = ( points [ i ] . x + points [ i + 1 ] . x ) / 2 ;
110+ const midY = ( points [ i ] . y + points [ i + 1 ] . y ) / 2 ;
111+ linePath += ` Q ${ points [ i ] . x . toFixed ( 2 ) } ${ points [ i ] . y . toFixed ( 2 ) } ${ midX . toFixed ( 2 ) } ${ midY . toFixed ( 2 ) } ` ;
112+ }
113+ const last = points [ points . length - 1 ] ;
114+ linePath += ` T ${ last . x . toFixed ( 2 ) } ${ last . y . toFixed ( 2 ) } ` ;
115+
116+ const xGuides = [ 20 , 100 , 1000 , 10000 ]
117+ . map ( ( freq ) => {
118+ const x = ( ( Math . log10 ( freq ) - minLog ) / spanLog ) * 100 ;
119+ if ( ! Number . isFinite ( x ) || x < 0 || x > 100 ) {
120+ return null ;
121+ }
122+ const label = freq >= 1000 ? `${ Math . round ( freq / 1000 ) } k` : `${ freq } ` ;
123+ return { x, label } ;
87124 } )
88- . join ( " " ) ;
125+ . filter ( ( entry ) : entry is { x : number ; label : string } => entry !== null ) ;
126+
127+ return {
128+ linePath,
129+ areaPath : `${ linePath } L 100 100 L 0 100 Z` ,
130+ xGuides
131+ } ;
89132 } ) ( ) ;
90133
91134 useEffect ( ( ) => {
@@ -219,71 +262,35 @@ export function SweepFrPage({
219262 < p className = "muted" style = { { marginTop : 0 } } >
220263 { monitor . status }
221264 </ p >
222- < div className = "field-grid-2" style = { { marginBottom : 12 } } >
223- < div >
224- < div className = "level-meter" style = { { marginBottom : 12 } } >
225- < div className = "level-meter-grid" />
226- < div className = "level-meter-bars" >
227- { meterHistory . map ( ( level , index ) => (
228- < span
229- key = { `meter-${ index } ` }
230- className = { `level-meter-bar ${
231- level > 0.92 ? "is-hot" : level > 0.72 ? "is-warm" : ""
232- } `. trim ( ) }
233- style = { {
234- height : `${ Math . max ( 8 , level * 100 ) } %`
235- } }
236- />
237- ) ) }
238- </ div >
239- < span className = "level-meter-peak" style = { { left : `${ peakNorm * 100 } %` } } />
240- </ div >
241- < div className = "field-grid-3" >
242- < div className = "field-row" >
243- < span className = "field-label" > Current Level</ span >
244- < strong > { monitor . currentDbfs . toFixed ( 1 ) } dBFS</ strong >
245- </ div >
246- < div className = "field-row" >
247- < span className = "field-label" > Peak Level</ span >
248- < strong > { monitor . peakDbfs . toFixed ( 1 ) } dBFS</ strong >
249- </ div >
250- < div className = "field-row" >
251- < span className = "field-label" > SPL Estimate</ span >
252- < strong > { monitor . splEstimate . toFixed ( 1 ) } dB SPL</ strong >
253- </ div >
254- </ div >
265+ < div className = "level-meter" style = { { marginBottom : 12 } } >
266+ < div className = "level-meter-grid" />
267+ < div className = "level-meter-bars" >
268+ { meterHistory . map ( ( level , index ) => (
269+ < span
270+ key = { `meter-${ index } ` }
271+ className = { `level-meter-bar ${
272+ level > 0.92 ? "is-hot" : level > 0.72 ? "is-warm" : ""
273+ } `. trim ( ) }
274+ style = { {
275+ height : `${ Math . max ( 8 , level * 100 ) } %`
276+ } }
277+ />
278+ ) ) }
255279 </ div >
256- < div className = "level-meter" >
257- < div
258- style = { {
259- display : "flex" ,
260- justifyContent : "space-between" ,
261- alignItems : "center" ,
262- marginBottom : 8
263- } }
264- >
265- < span className = "field-label" > Live Rough FR (Pink Noise)</ span >
266- < span className = "muted" > { pinkNoisePlaying ? "Live" : "Idle" } </ span >
267- </ div >
268- < svg viewBox = "0 0 100 100" style = { { width : "100%" , height : 100 , display : "block" } } >
269- < line x1 = "0" y1 = "50" x2 = "100" y2 = "50" stroke = "var(--level-grid)" strokeWidth = "1" />
270- < line x1 = "0" y1 = "25" x2 = "100" y2 = "25" stroke = "var(--level-grid)" strokeWidth = "0.6" />
271- < line x1 = "0" y1 = "75" x2 = "100" y2 = "75" stroke = "var(--level-grid)" strokeWidth = "0.6" />
272- { pinkNoisePlaying && roughFrPath ? (
273- < polyline
274- points = { roughFrPath }
275- fill = "none"
276- stroke = "var(--accent-strong)"
277- strokeWidth = "1.8"
278- strokeLinejoin = "round"
279- strokeLinecap = "round"
280- />
281- ) : (
282- < text x = "50" y = "54" textAnchor = "middle" fontSize = "7" fill = "var(--text-muted)" >
283- Start Pink Noise + Monitoring
284- </ text >
285- ) }
286- </ svg >
280+ < span className = "level-meter-peak" style = { { left : `${ peakNorm * 100 } %` } } />
281+ </ div >
282+ < div className = "field-grid-3" >
283+ < div className = "field-row" >
284+ < span className = "field-label" > Current Level</ span >
285+ < strong > { monitor . currentDbfs . toFixed ( 1 ) } dBFS</ strong >
286+ </ div >
287+ < div className = "field-row" >
288+ < span className = "field-label" > Peak Level</ span >
289+ < strong > { monitor . peakDbfs . toFixed ( 1 ) } dBFS</ strong >
290+ </ div >
291+ < div className = "field-row" >
292+ < span className = "field-label" > SPL Estimate</ span >
293+ < strong > { monitor . splEstimate . toFixed ( 1 ) } dB SPL</ strong >
287294 </ div >
288295 </ div >
289296 { monitor . clipCount > 0 && (
@@ -314,42 +321,98 @@ export function SweepFrPage({
314321 </ section >
315322
316323 < section className = "page-card" >
317- < h3 className = "section-subheading" > Sweep FR Results</ h3 >
318- < div className = "scroll-box" style = { { minHeight : 468 , maxHeight : 468 } } >
319- < pre className = "mono-pre" >
320- { lastResult
321- ? JSON . stringify ( lastResult , null , 2 )
322- : "No sweep result yet. Run Sweep to populate this panel." }
323- </ pre >
324- </ div >
325- < div className = "row-end" style = { { marginTop : 12 } } >
326- < button
327- type = "button"
328- className = "skin-btn secondary"
329- disabled = { running || ! hasSweepResult }
330- onClick = { onExportLastJson }
331- >
332- Export LAST (JSON)
333- </ button >
334- < button
335- type = "button"
336- className = "skin-btn secondary"
337- disabled = { running || ! hasSweepHistory }
338- onClick = { onExportAllJson }
339- >
340- Export ALL (JSON)
341- </ button >
342- < button
343- type = "button"
344- className = "skin-btn secondary"
345- disabled = { running || ! hasSweepResult }
346- onClick = { onExportLastSquiglink }
347- >
348- Export LAST to Squiglink
349- </ button >
324+ < h3 className = "section-subheading" > Live Rough FR (Pink Noise)</ h3 >
325+ < p className = "muted" style = { { marginTop : 0 } } >
326+ { pinkNoisePlaying ? "Live preview running" : "Start Pink Noise + Monitoring" }
327+ </ p >
328+ < div className = "level-meter" >
329+ < svg viewBox = "0 0 100 100" style = { { width : "100%" , height : 128 , display : "block" } } >
330+ < line x1 = "0" y1 = "10" x2 = "100" y2 = "10" stroke = "var(--level-grid)" strokeWidth = "0.6" />
331+ < line x1 = "0" y1 = "50" x2 = "100" y2 = "50" stroke = "var(--level-grid)" strokeWidth = "1" />
332+ < line x1 = "0" y1 = "90" x2 = "100" y2 = "90" stroke = "var(--level-grid)" strokeWidth = "0.6" />
333+ { ( roughFrGraph ?. xGuides ?? [ ] ) . map ( ( guide ) => (
334+ < g key = { `guide-${ guide . label } -${ guide . x . toFixed ( 2 ) } ` } >
335+ < line
336+ x1 = { guide . x }
337+ y1 = "8"
338+ x2 = { guide . x }
339+ y2 = "92"
340+ stroke = "var(--level-grid)"
341+ strokeWidth = "0.45"
342+ />
343+ < text x = { guide . x } y = "98" textAnchor = "middle" fontSize = "6" fill = "var(--text-muted)" >
344+ { guide . label }
345+ </ text >
346+ </ g >
347+ ) ) }
348+ < text x = "2" y = "12" fontSize = "6" fill = "var(--text-muted)" >
349+ +18 dB
350+ </ text >
351+ < text x = "2" y = "52" fontSize = "6" fill = "var(--text-muted)" >
352+ 0 dB
353+ </ text >
354+ < text x = "2" y = "92" fontSize = "6" fill = "var(--text-muted)" >
355+ -18 dB
356+ </ text >
357+ { pinkNoisePlaying && roughFrGraph ? (
358+ < >
359+ < path d = { roughFrGraph . areaPath } fill = "var(--accent-dim)" opacity = "0.2" />
360+ < path
361+ d = { roughFrGraph . linePath }
362+ fill = "none"
363+ stroke = "var(--accent-strong)"
364+ strokeWidth = "2"
365+ strokeLinejoin = "round"
366+ strokeLinecap = "round"
367+ />
368+ </ >
369+ ) : (
370+ < text x = "50" y = "54" textAnchor = "middle" fontSize = "7" fill = "var(--text-muted)" >
371+ Waiting for live data
372+ </ text >
373+ ) }
374+ </ svg >
350375 </ div >
351376 </ section >
352377 </ div >
378+
379+ < section className = "page-card" style = { { marginTop : 12 } } >
380+ < h3 className = "section-subheading" > Sweep FR Results</ h3 >
381+ < div className = "scroll-box" style = { { minHeight : 468 , maxHeight : 468 } } >
382+ < pre className = "mono-pre" >
383+ { lastResult
384+ ? JSON . stringify ( lastResult , null , 2 )
385+ : "No sweep result yet. Run Sweep to populate this panel." }
386+ </ pre >
387+ </ div >
388+ < div className = "row-end" style = { { marginTop : 12 } } >
389+ < button
390+ type = "button"
391+ className = "skin-btn secondary"
392+ disabled = { running || ! hasSweepResult }
393+ onClick = { onExportLastJson }
394+ >
395+ Export LAST (JSON)
396+ </ button >
397+ < button
398+ type = "button"
399+ className = "skin-btn secondary"
400+ disabled = { running || ! hasSweepHistory }
401+ onClick = { onExportAllJson }
402+ >
403+ Export ALL (JSON)
404+ </ button >
405+ < button
406+ type = "button"
407+ className = "skin-btn secondary"
408+ disabled = { running || ! hasSweepResult }
409+ onClick = { onExportLastSquiglink }
410+ >
411+ Export LAST to Squiglink
412+ </ button >
413+ </ div >
414+ </ section >
415+
353416 </ section >
354417 </ div >
355418 ) ;
0 commit comments