@@ -547,8 +547,8 @@ function TemperamentWidget() {
547547 const i = Number ( event . target . dataset . message ) ;
548548 const that = this ;
549549
550- docById ( "noteInfo" ) . style . width = "180px " ;
551- docById ( "noteInfo" ) . style . height = "130px " ;
550+ docById ( "noteInfo" ) . style . width = "200px " ;
551+ docById ( "noteInfo" ) . style . height = "240px " ;
552552 if ( docById ( "note" ) ) docById ( "note" ) . textContent = "" ;
553553 if ( docById ( "frequency" ) ) docById ( "frequency" ) . textContent = "" ;
554554
@@ -574,26 +574,55 @@ function TemperamentWidget() {
574574 freqSpan . textContent = this . frequencies [ i ] ;
575575 noteInfo . appendChild ( freqSpan ) ;
576576
577+ // Cents input (relative to the starting pitch value at this position).
578+ // Musicians typically think in cents rather than hertz, so offer both.
579+ const originalFrequency = parseFloat ( this . frequencies [ i ] ) ;
580+ noteInfo . appendChild ( document . createElement ( "br" ) ) ;
581+ noteInfo . appendChild ( document . createTextNode ( `\u00A0\u00A0${ _ ( "cents" ) } ` ) ) ;
582+ const centsInput = document . createElement ( "input" ) ;
583+ centsInput . type = "number" ;
584+ centsInput . id = "centsInput1" ;
585+ centsInput . value = "0" ;
586+ centsInput . step = "1" ;
587+ // Match the frequency display styling: blue text on grey rounded pill.
588+ centsInput . className = "rangeslidervalue" ;
589+ centsInput . style . width = "60px" ;
590+ centsInput . style . textAlign = "center" ;
591+ centsInput . style . border = "none" ;
592+ noteInfo . appendChild ( centsInput ) ;
593+
577594 noteInfo . appendChild ( document . createElement ( "br" ) ) ;
578595 noteInfo . appendChild ( document . createElement ( "br" ) ) ;
579596 const doneDiv = document . createElement ( "div" ) ;
580597 doneDiv . id = "done" ;
581- doneDiv . style . background = "rgb(196, 196, 196)" ;
598+ doneDiv . style . background = "#64B5F6" ;
599+ doneDiv . style . padding = "6px" ;
600+ doneDiv . style . marginTop = "8px" ;
601+ doneDiv . style . borderRadius = "4px" ;
602+ doneDiv . style . cursor = "pointer" ;
582603 const doneCenter = document . createElement ( "center" ) ;
583604 doneCenter . textContent = _ ( "done" ) ;
605+ doneCenter . style . color = "white" ;
606+ doneCenter . style . fontWeight = "bold" ;
584607 doneDiv . appendChild ( doneCenter ) ;
585608 noteInfo . appendChild ( doneDiv ) ;
586609
587- docById ( "frequencySlider1" ) . oninput = function ( ) {
588- docById ( "frequencydiv1" ) . textContent = docById ( "frequencySlider1" ) . value ;
589- const frequency = docById ( "frequencySlider1" ) . value ;
590- const ratio = frequency / that . frequencies [ 0 ] ;
610+ // Convert between frequency and cents using the widget-level helpers
611+ // (see _freqToCents / _centsToFreq) so the math is testable.
612+ const freqToCents = freq => that . _freqToCents ( freq , originalFrequency ) ;
613+ const centsToFreq = cents => that . _centsToFreq ( cents , originalFrequency ) ;
614+
615+ // Shared update logic so both inputs behave identically.
616+ const applyFrequency = frequency => {
617+ const freqNum = parseFloat ( frequency ) ;
618+ docById ( "frequencydiv1" ) . textContent = freqNum . toFixed ( 2 ) ;
619+ const ratio = freqNum / that . frequencies [ 0 ] ;
591620 that . temporaryRatios = that . ratios . slice ( ) ;
592621 that . temporaryRatios [ i ] = ratio ;
593622 that . _logo . resetSynth ( 0 ) ;
594623 that . _logo . synth . trigger (
595624 0 ,
596- frequency ,
625+ freqNum ,
597626 Singer . defaultBPMFactor * 0.01 ,
598627 "electronic synth" ,
599628 null ,
@@ -602,6 +631,26 @@ function TemperamentWidget() {
602631 that . createMainWheel ( that . temporaryRatios ) ;
603632 } ;
604633
634+ docById ( "frequencySlider1" ) . oninput = function ( ) {
635+ const frequency = parseFloat ( docById ( "frequencySlider1" ) . value ) ;
636+ // Keep cents box in sync without triggering its own handler.
637+ docById ( "centsInput1" ) . value = Math . round ( freqToCents ( frequency ) ) ;
638+ applyFrequency ( frequency ) ;
639+ } ;
640+
641+ docById ( "centsInput1" ) . oninput = function ( ) {
642+ const cents = parseFloat ( docById ( "centsInput1" ) . value ) ;
643+ if ( isNaN ( cents ) ) return ;
644+ const sliderEl = docById ( "frequencySlider1" ) ;
645+ const min = parseFloat ( sliderEl . getAttribute ( "min" ) ) ;
646+ const max = parseFloat ( sliderEl . getAttribute ( "max" ) ) ;
647+ // Clamp the resulting frequency to the slider's allowed range so
648+ // user cannot move the pitch outside the neighbour boundaries.
649+ const frequency = Math . min ( Math . max ( centsToFreq ( cents ) , min ) , max ) ;
650+ sliderEl . value = frequency ;
651+ applyFrequency ( frequency ) ;
652+ } ;
653+
605654 docById ( "done" ) . onclick = function ( ) {
606655 that . ratios = that . temporaryRatios . slice ( ) ;
607656 that . typeOfEdit = "nonequal" ;
@@ -622,6 +671,29 @@ function TemperamentWidget() {
622671 } ;
623672 } ;
624673
674+ /**
675+ * Converts a frequency (Hz) to cents offset relative to a base frequency.
676+ * 0 cents means the frequency equals the base; +1200 is one octave up;
677+ * +100 is one semitone up.
678+ * @param {number } freq - The frequency in Hz.
679+ * @param {number } baseFreq - The reference frequency in Hz.
680+ * @returns {number } The cents offset from base.
681+ */
682+ this . _freqToCents = function ( freq , baseFreq ) {
683+ return 1200 * Math . log2 ( freq / baseFreq ) ;
684+ } ;
685+
686+ /**
687+ * Converts a cents offset back to an absolute frequency in Hz relative
688+ * to a base frequency.
689+ * @param {number } cents - The cents offset from base.
690+ * @param {number } baseFreq - The reference frequency in Hz.
691+ * @returns {number } The resulting frequency in Hz.
692+ */
693+ this . _centsToFreq = function ( cents , baseFreq ) {
694+ return baseFreq * Math . pow ( 2 , cents / 1200 ) ;
695+ } ;
696+
625697 /**
626698 * Switches to displaying the graph of notes on the temperament widget.
627699 * @returns {void }
0 commit comments