feat: add custom tempo/BPM converter with musical note units

Add comprehensive tempo conversion system supporting:
- BPM (beats per minute) as base unit
- Musical note durations: whole, half, quarter, eighth, sixteenth, thirty-second
- Dotted notes (1.5x duration): dotted-half, dotted-quarter, dotted-eighth, dotted-sixteenth
- Triplet notes (2/3 duration): quarter-triplet, eighth-triplet, sixteenth-triplet
- Time units: milliseconds, seconds, Hertz

Technical implementation:
- Created lib/tempo.ts with custom measure definition
- Extended lib/units.ts with tempo integration and reciprocal conversion logic
- Added tempo category color (orange #F97316) to globals.css
- Conversion formula: milliseconds per beat = 60000 / BPM
- Special handling for BPM ↔ time unit conversions using reciprocal relationship

The tempo converter integrates seamlessly with existing UX:
- Appears as "Tempo / BPM" category in category selector
- Supports all features: search, favorites, history, visual comparison, draggable bars
- Enables musicians to convert between BPM and note durations for tempo calculations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-08 11:53:50 +01:00
parent 296f78709b
commit 75f895284f
3 changed files with 169 additions and 1 deletions

View File

@@ -4,6 +4,7 @@
*/
import convert from 'convert-units';
import { tempoMeasure } from './tempo';
export type Measure =
| 'angle'
@@ -25,6 +26,7 @@ export type Measure =
| 'reactivePower'
| 'speed'
| 'temperature'
| 'tempo'
| 'time'
| 'voltage'
| 'volume'
@@ -48,13 +50,17 @@ export interface ConversionResult {
* Get all available measures/categories
*/
export function getAllMeasures(): Measure[] {
return convert().measures() as Measure[];
const standardMeasures = convert().measures() as Measure[];
return [...standardMeasures, 'tempo'];
}
/**
* Get all units for a specific measure
*/
export function getUnitsForMeasure(measure: Measure): string[] {
if (measure === 'tempo') {
return Object.keys(tempoMeasure.tempo.systems.metric);
}
return convert().possibilities(measure);
}
@@ -63,6 +69,19 @@ export function getUnitsForMeasure(measure: Measure): string[] {
*/
export function getUnitInfo(unit: string): UnitInfo | null {
try {
// Check if it's a tempo unit
const tempoUnits = tempoMeasure.tempo.systems.metric;
if (unit in tempoUnits) {
const tempoUnit = tempoUnits[unit as keyof typeof tempoUnits];
return {
abbr: unit,
measure: 'tempo',
system: 'metric',
singular: tempoUnit.name.singular,
plural: tempoUnit.name.plural,
};
}
const description = convert().describe(unit);
return description as UnitInfo;
} catch {
@@ -79,6 +98,34 @@ export function convertUnit(
toUnit: string
): number {
try {
// Handle tempo conversions
const tempoUnits = tempoMeasure.tempo.systems.metric;
const isFromTempo = fromUnit in tempoUnits;
const isToTempo = toUnit in tempoUnits;
if (isFromTempo && isToTempo) {
const fromAnchor = tempoUnits[fromUnit as keyof typeof tempoUnits].to_anchor;
const toAnchor = tempoUnits[toUnit as keyof typeof tempoUnits].to_anchor;
// Special handling for BPM conversions (reciprocal relationship)
if (fromUnit === 'BPM') {
// BPM → time unit: divide anchor by BPM value
// Example: 120 BPM → quarter = 60000 / 120 = 500ms
return toAnchor / value;
} else if (toUnit === 'BPM') {
// Time unit → BPM: divide anchor by time value
// Example: 500ms quarter → BPM = 60000 / 500 = 120
return fromAnchor / value;
} else {
// Time unit → time unit: convert through BPM
// Example: 500ms quarter → eighth
// Step 1: 500ms → BPM = 60000 / 500 = 120
// Step 2: 120 BPM → eighth = 30000 / 120 = 250ms
const bpm = fromAnchor / value;
return toAnchor / bpm;
}
}
return convert(value).from(fromUnit).to(toUnit);
} catch (error) {
console.error('Conversion error:', error);
@@ -139,6 +186,7 @@ export function getCategoryColor(measure: Measure): string {
reactivePower: 'category-reactive-power',
speed: 'category-speed',
temperature: 'category-temperature',
tempo: 'category-tempo',
time: 'category-time',
voltage: 'category-voltage',
volume: 'category-volume',
@@ -172,6 +220,7 @@ export function getCategoryColorHex(measure: Measure): string {
reactivePower: '#E879F9',
speed: '#10B981',
temperature: '#EF4444',
tempo: '#F97316', // Orange for music/tempo
time: '#7C3AED',
voltage: '#FB923C',
volume: '#8B5CF6',
@@ -205,6 +254,7 @@ export function formatMeasureName(measure: Measure): string {
reactivePower: 'Reactive Power',
speed: 'Speed',
temperature: 'Temperature',
tempo: 'Tempo / BPM',
time: 'Time',
voltage: 'Voltage',
volume: 'Volume',