@@ -4745,50 +4745,115 @@ function getNote(
47454745}
47464746
47474747/**
4748- * Calculate the pitch number based on the activity, pitch value, and tur parameters.
4749- * @function
4750- * @param {Object } activity - The activity object.
4751- * @param {string|number } np - The pitch value (note name or frequency in Hertz).
4752- * @param {Object } tur - The tur parameters containing singer information.
4753- * @returns {number } The calculated pitch number.
4748+ * Maps accidental characters to their semitone offsets.
4749+ * Named distinctly to avoid collision with the ACCIDENTAL_MAP in abc.js
4750+ * (which maps accidentals to ABC notation strings, not semitone offsets).
4751+ * @constant {Object.<string, number>}
47544752 */
4755- const _calculate_pitch_number = ( activity , np , tur ) => {
4756- let obj ;
4757- if ( tur . singer . lastNotePlayed !== null ) {
4758- if ( typeof np === "string" ) {
4759- obj = noteToObj ( np ) ;
4760- } else {
4761- // Hertz
4762- obj = frequencyToPitch ( np ) ;
4753+ const ACCIDENTAL_SEMITONE_MAP = {
4754+ "#" : 1 ,
4755+ "♯" : 1 ,
4756+ "b" : - 1 ,
4757+ "♭" : - 1 ,
4758+ "x" : 2 , // double-sharp (textual)
4759+ "𝄪" : 2 , // double-sharp (Unicode)
4760+ "𝄫" : - 2 // double-flat (Unicode)
4761+ } ;
4762+
4763+ /**
4764+ * Parses a pitch string into its note name and octave components.
4765+ * Handles single and double accidentals (#, b, ♯, ♭, x, 𝄪, 𝄫).
4766+ * @param {string } str - The pitch string (e.g. "C4", "A#10", "F𝄪5").
4767+ * @returns {Array } An array containing [normalizedNoteName, octave].
4768+ */
4769+ function _parse_pitch_string ( str ) {
4770+ const match = str . match ( / ^ ( [ A - G a - g ] ) ( [ # b ♭ ♯ 𝄪 𝄫 x ] * ) ( \- ? \d + ) $ / ) ;
4771+ if ( match ) {
4772+ const baseLetter = match [ 1 ] . toUpperCase ( ) ;
4773+ const accidentalStr = match [ 2 ] ;
4774+ const octave = parseInt ( match [ 3 ] , 10 ) ;
4775+
4776+ if ( ! accidentalStr ) {
4777+ // No accidentals; return as-is
4778+ return [ baseLetter , octave ] ;
47634779 }
4764- } else if (
4765- tur . singer . inNoteBlock in tur . singer . notePitches &&
4766- tur . singer . notePitches [ last ( tur . singer . inNoteBlock ) ] . length > 0
4767- ) {
4768- obj = getNote (
4769- tur . singer . notePitches [ last ( tur . singer . inNoteBlock ) ] [ 0 ] ,
4770- tur . singer . noteOctaves [ last ( tur . singer . inNoteBlock ) ] [ 0 ] ,
4771- 0 ,
4772- tur . singer . keySignature ,
4773- tur . singer . movable ,
4774- null ,
4775- activity . errorMsg
4780+
4781+ // Sum up semitone offsets for all accidental characters.
4782+ // 𝄪 and 𝄫 are multi-byte but treated as single code points by spread.
4783+ const accidentalChars = [ ...accidentalStr ] ;
4784+ const semitoneOffset = accidentalChars . reduce (
4785+ ( sum , char ) => sum + ( ACCIDENTAL_SEMITONE_MAP [ char ] || 0 ) ,
4786+ 0
47764787 ) ;
4777- } else {
4778- try {
4779- if ( typeof np === "string" ) {
4780- obj = noteToObj ( np ) ;
4781- } else {
4782- // Hertz
4783- obj = frequencyToPitch ( np ) ;
4784- }
4785- } catch ( e ) {
4786- activity . errorMsg ( INVALIDPITCH ) ;
4787- obj = [ "G" , 4 ] ;
4788+
4789+ // Build a normalized name using canonical SHARP/FLAT symbols.
4790+ let normalizedName ;
4791+ if ( semitoneOffset === 0 ) {
4792+ normalizedName = baseLetter ;
4793+ } else if ( semitoneOffset === 1 ) {
4794+ normalizedName = baseLetter + SHARP ;
4795+ } else if ( semitoneOffset === - 1 ) {
4796+ normalizedName = baseLetter + FLAT ;
4797+ } else if ( semitoneOffset === 2 ) {
4798+ normalizedName = baseLetter + DOUBLESHARP ;
4799+ } else if ( semitoneOffset === - 2 ) {
4800+ normalizedName = baseLetter + DOUBLEFLAT ;
4801+ } else {
4802+ // For unusual combinations, keep semitone representation as extra sharps/flats
4803+ const sym = semitoneOffset > 0 ? SHARP : FLAT ;
4804+ normalizedName = baseLetter + sym . repeat ( Math . abs ( semitoneOffset ) ) ;
47884805 }
4806+
4807+ return [ normalizedName , octave ] ;
4808+ }
4809+ // Fallback: no octave digit found – normalize accidentals and default octave to 4
4810+ return [ str . replace ( "#" , SHARP ) . replace ( "b" , FLAT ) , 4 ] ;
4811+ }
4812+
4813+ /**
4814+ * Calculates a pitch number from a note name and octave.
4815+ * @param {string } noteName - The name of the note (e.g. "C", "C#").
4816+ * @param {number } octave - The octave number.
4817+ * @returns {number|string } The calculated pitch number or INVALIDPITCH if calculation fails.
4818+ */
4819+ function _calculate_pitch_number ( noteName , octave ) {
4820+ if ( typeof noteName !== "string" ) {
4821+ return INVALIDPITCH ;
4822+ }
4823+
4824+ let name = noteName . replace ( "#" , SHARP ) . replace ( "b" , FLAT ) ;
4825+
4826+ // Handle double accidentals (𝄪 / 𝄫) by computing offset directly from
4827+ // the base note letter, since they won't appear in NOTESSHARP/NOTESFLAT.
4828+ if ( name . includes ( DOUBLESHARP ) || name . includes ( DOUBLEFLAT ) ) {
4829+ const offset = name . includes ( DOUBLESHARP ) ? 2 : - 2 ;
4830+ const baseLetter = name . replace ( DOUBLESHARP , "" ) . replace ( DOUBLEFLAT , "" ) ;
4831+ let baseIndex = NOTESSHARP . indexOf ( baseLetter ) ;
4832+ if ( baseIndex === - 1 ) baseIndex = NOTESFLAT . indexOf ( baseLetter ) ;
4833+ if ( baseIndex === - 1 ) return INVALIDPITCH ;
4834+ const rawPitch = ( parseInt ( octave , 10 ) + 1 ) * 12 + baseIndex + offset ;
4835+ return rawPitch ;
4836+ }
4837+
4838+ if ( EQUIVALENTSHARPS [ name ] ) {
4839+ name = EQUIVALENTSHARPS [ name ] ;
4840+ } else if ( EQUIVALENTFLATS [ name ] ) {
4841+ name = EQUIVALENTFLATS [ name ] ;
4842+ } else if ( EQUIVALENTNATURALS [ name ] ) {
4843+ name = EQUIVALENTNATURALS [ name ] ;
4844+ }
4845+
4846+ let pitchIndex = NOTESSHARP . indexOf ( name ) ;
4847+ if ( pitchIndex === - 1 ) {
4848+ pitchIndex = NOTESFLAT . indexOf ( name ) ;
4849+ }
4850+
4851+ if ( pitchIndex === - 1 ) {
4852+ return INVALIDPITCH ;
47894853 }
4790- return pitchToNumber ( obj [ 0 ] , obj [ 1 ] , tur . singer . keySignature ) - tur . singer . pitchNumberOffset ;
4791- } ;
4854+
4855+ return ( parseInt ( octave , 10 ) + 1 ) * 12 + pitchIndex ;
4856+ }
47924857
47934858/**
47944859 * Build the scale based on the given key signature.
@@ -6120,11 +6185,44 @@ const convertFactor = factor => {
61206185 * @param {* } tur - The tur object.
61216186 * @returns {* } The pitch information based on the specified type.
61226187 */
6123- const getPitchInfo = ( activity , type , currentNote , tur ) => {
6124- // A variety of conversions.
6188+ /**
6189+ * Get pitch information based on the note or pitch provided.
6190+ * @function
6191+ * @param {string|number } noteOrPitch - The note name (e.g. "C4") or a numeric pitch index.
6192+ * @returns {Object|string } If called with one argument, returns { name, octave, pitchNumber }. Otherwise returns legacy values.
6193+ */
6194+ const getPitchInfo = function ( activity , type , currentNote , tur ) {
6195+ if ( arguments . length === 1 ) {
6196+ const noteOrPitch = activity ;
6197+ let name , octave , pitchNumber ;
6198+
6199+ if ( typeof noteOrPitch === "number" ) {
6200+ pitchNumber = noteOrPitch ;
6201+ octave = Math . floor ( pitchNumber / 12 ) - 1 ;
6202+ name = NOTESSHARP [ pitchNumber % 12 ] ;
6203+ } else if ( typeof noteOrPitch === "string" ) {
6204+ [ name , octave ] = _parse_pitch_string ( noteOrPitch ) ;
6205+ pitchNumber = _calculate_pitch_number ( name , octave ) ;
6206+ } else {
6207+ return INVALIDPITCH ;
6208+ }
6209+
6210+ if ( pitchNumber === INVALIDPITCH ) {
6211+ return { name : null , octave : null , pitchNumber : INVALIDPITCH } ;
6212+ }
6213+
6214+ return {
6215+ name : name . replace ( SHARP , "#" ) . replace ( FLAT , "b" ) ,
6216+ octave : parseInt ( octave , 10 ) ,
6217+ pitchNumber : pitchNumber
6218+ } ;
6219+ }
6220+
6221+ // Legacy behavior for 4 arguments
61256222 let pitch ;
61266223 let octave ;
61276224 let obj ;
6225+ let cents ;
61286226 if ( Number ( currentNote ) ) {
61296227 // If it is a frequency, convert it to a pitch/octave.
61306228 obj = frequencyToPitch ( currentNote ) ;
@@ -6133,8 +6231,7 @@ const getPitchInfo = (activity, type, currentNote, tur) => {
61336231 cents = obj [ 2 ] ;
61346232 } else {
61356233 // Turn the note into pitch and octave.
6136- pitch = currentNote . substr ( 0 , currentNote . length - 1 ) ;
6137- octave = currentNote [ currentNote . length - 1 ] ;
6234+ [ pitch , octave ] = _parse_pitch_string ( currentNote ) ;
61386235 }
61396236 // Remap double sharps/double flats.
61406237 if ( pitch . includes ( DOUBLESHARP ) ) {
@@ -6173,7 +6270,7 @@ const getPitchInfo = (activity, type, currentNote, tur) => {
61736270 case "solfege class" :
61746271 if ( type === "solfege class" ) {
61756272 // Remove sharps and flats.
6176- pitch = pitch . replace ( SHARP ) . replace ( FLAT ) ;
6273+ pitch = pitch . replace ( SHARP , "" ) . replace ( FLAT , "" ) ;
61776274 }
61786275 if ( tur . singer . movable === false ) {
61796276 return SOLFEGECONVERSIONTABLE [ pitch ] ;
@@ -6205,7 +6302,7 @@ const getPitchInfo = (activity, type, currentNote, tur) => {
62056302 ( octave - 4 ) * YSTAFFOCTAVEHEIGHT
62066303 ) ;
62076304 case "pitch number" :
6208- return _calculate_pitch_number ( activity , pitch , tur ) ;
6305+ return _calculate_pitch_number ( pitch , octave ) ;
62096306 case "pitch in hertz" :
62106307 // This function ignores cents.
62116308 return activity . logo . synth . _getFrequency (
0 commit comments