import * as alphaTab from '@coderline/alphatab'; /** * Result structure representing a successfully mapped Songsterr duration in alphaTab terms. */ export interface DurationMappingResult { duration: alphaTab.model.Duration; // Closest matching alphaTab standard duration enum value dots: number; // Number of dot annotations (0, 1, or 2) isApproximate: boolean; // Flag indicating if the duration had to be approximated due to custom fractions } // Available base durations supported natively by alphaTab and standard GP files const baseDurations: alphaTab.model.Duration[] = [ alphaTab.model.Duration.Whole, alphaTab.model.Duration.Half, alphaTab.model.Duration.Quarter, alphaTab.model.Duration.Eighth, alphaTab.model.Duration.Sixteenth, alphaTab.model.Duration.ThirtySecond, alphaTab.model.Duration.SixtyFourth ]; /** * Maps a fractional duration tuple [numerator, denominator] from Songsterr's API payload * to a standardized alphaTab.model.Duration plus any dot values (dotted or double-dotted notes). * * Since Songsterr can utilize arbitrary fractional times, this searches through a * matrix of valid base durations combined with 0 to 2 dots, selecting the combination * that yields the minimum numeric delta. * * @param duration An array containing [numerator, denominator] representing a musical time slice. * @returns A DurationMappingResult containing mapped duration, dots, and a precision flag. */ export function mapSongsterrDuration( duration: [number, number] | undefined ): DurationMappingResult { // If no duration is provided, default to a standard quarter note and mark it as approximate. if (!duration) { return { duration: alphaTab.model.Duration.Quarter, dots: 0, isApproximate: true }; } const [numerator, denominator] = duration; if (!numerator || !denominator) { return { duration: alphaTab.model.Duration.Quarter, dots: 0, isApproximate: true }; } // Calculate the numeric target duration value (e.g., 1/4 = 0.25, 3/8 = 0.375) const targetValue = numerator / denominator; let bestDuration = alphaTab.model.Duration.Quarter; let bestDots = 0; let bestDelta = Number.POSITIVE_INFINITY; // Search space: check each base duration against 0, 1, or 2 dots. for (const candidateDuration of baseDurations) { // Convert duration enum back to its numeric value (e.g., Duration.Quarter has underlying enum value of 4 -> 1/4) const baseValue = 1 / Number(candidateDuration); for (const dots of [0, 1, 2]) { // Dotted notes add half of the preceding value: // 1 dot = base + base/2 // 2 dots = base + base/2 + base/4 const dottedValue = baseValue + (dots >= 1 ? baseValue / 2 : 0) + (dots >= 2 ? baseValue / 4 : 0); const delta = Math.abs(dottedValue - targetValue); // Track the closest match if (delta < bestDelta) { bestDelta = delta; bestDuration = candidateDuration; bestDots = dots; } } } return { duration: bestDuration, dots: bestDots, // Consider it exact if the delta is within a minuscule threshold isApproximate: bestDelta > 0.000001 }; }