Files
tab-downloader/songsterr/duration-mapper.ts

92 lines
3.2 KiB
TypeScript

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
};
}