import { MIDIValidationError } from "../core/errors.js"
/**
* Clamp a value between min and max
* @param {number} value - Value to clamp
* @param {number} min - Minimum value
* @param {number} max - Maximum value
* @returns {number}
*/
export function clamp(value, min, max) {
return Math.max(min, Math.min(max, value))
}
/**
* Normalize a value from input range to MIDI range (0-127)
* @param {number} value - Input value
* @param {number} inputMin - Input minimum
* @param {number} inputMax - Input maximum
* @param {boolean} [invert=false] - Invert the output
* @returns {number} MIDI value (0-127)
*/
export function normalizeValue(value, inputMin, inputMax, invert = false) {
const normalized = (value - inputMin) / (inputMax - inputMin)
const final = invert ? 1 - normalized : normalized
const midiValue = final * 127
return clamp(Math.round(midiValue), 0, 127)
}
/**
* Denormalize a MIDI value (0-127) to a custom range
* @param {number} midiValue - MIDI value (0-127)
* @param {number} outputMin - Output minimum
* @param {number} outputMax - Output maximum
* @param {boolean} [invert=false] - Invert the input
* @returns {number}
*/
export function denormalizeValue(midiValue, outputMin, outputMax, invert = false) {
let normalized = clamp(midiValue, 0, 127) / 127
if (invert) {
normalized = 1 - normalized
}
return outputMin + normalized * (outputMax - outputMin)
}
/**
* Convert a note name to MIDI note number
* @param {string} noteName - Note name (e.g., "C4", "A#3", "Bb5")
* @returns {number} MIDI note number (0-127)
* @throws {MIDIValidationError} If noteName is not a valid note format
*/
export function noteNameToNumber(noteName) {
const notes = {
C: 0,
"C#": 1,
DB: 1,
D: 2,
"D#": 3,
EB: 3,
E: 4,
F: 5,
"F#": 6,
GB: 6,
G: 7,
"G#": 8,
AB: 8,
A: 9,
"A#": 10,
BB: 10,
B: 11,
}
const match = noteName.match(/^([A-G][#b]?)(-?\d+)$/i)
if (!match) {
throw new MIDIValidationError(`Invalid note name: ${noteName}`, "note", noteName)
}
const [, note, octave] = match
const noteValue = notes[note.toUpperCase()]
if (noteValue === undefined) {
throw new MIDIValidationError(`Invalid note: ${note}`, "note", note)
}
const midiNote = (parseInt(octave, 10) + 1) * 12 + noteValue
return clamp(midiNote, 0, 127)
}
/**
* Convert MIDI note number to note name
* @param {number} noteNumber - MIDI note number (0-127)
* @param {boolean} [useFlats=false] - Use flats instead of sharps
* @returns {string} Note name (e.g., "C4")
*/
export function noteNumberToName(noteNumber, useFlats = false) {
const sharps = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
const flats = ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"]
const notes = useFlats ? flats : sharps
const octave = Math.floor(noteNumber / 12) - 1
const note = notes[noteNumber % 12]
return `${note}${octave}`
}
/**
* Convert frequency (Hz) to nearest MIDI note number
* @param {number} frequency - Frequency in Hz
* @returns {number} MIDI note number
*/
export function frequencyToNote(frequency) {
const noteNumber = 69 + 12 * Math.log2(frequency / 440)
return clamp(Math.round(noteNumber), 0, 127)
}
/**
* Convert MIDI note number to frequency (Hz)
* @param {number} noteNumber - MIDI note number
* @returns {number} Frequency in Hz
*/
export function noteToFrequency(noteNumber) {
return 440 * 2 ** ((noteNumber - 69) / 12)
}
/**
* Get CC name for common controller numbers
* @param {number} cc - CC number
* @returns {string} CC name or "CC {number}"
*/
export function getCCName(cc) {
const names = {
0: "Bank Select",
1: "Modulation",
2: "Breath Controller",
4: "Foot Controller",
5: "Portamento Time",
7: "Volume",
8: "Balance",
10: "Pan",
11: "Expression",
64: "Sustain Pedal",
65: "Portamento",
66: "Sostenuto",
67: "Soft Pedal",
68: "Legato",
71: "Resonance",
72: "Release Time",
73: "Attack Time",
74: "Cutoff",
75: "Decay Time",
76: "Vibrato Rate",
77: "Vibrato Depth",
78: "Vibrato Delay",
84: "Portamento Control",
91: "Reverb",
92: "Tremolo",
93: "Chorus",
94: "Detune",
95: "Phaser",
120: "All Sound Off",
121: "Reset All Controllers",
123: "All Notes Off",
}
return names[cc] || `CC ${cc}`
}
/**
* Encode a 14-bit value into MSB and LSB (7-bit each)
* @param {number} value - 14-bit value (0-16383)
* @returns {{msb: number, lsb: number}} MSB and LSB values
*/
export function encode14BitValue(value) {
const clamped = clamp(Math.round(value), 0, 16383)
return {
msb: (clamped >> 7) & 0x7f,
lsb: clamped & 0x7f,
}
}
/**
* Decode MSB and LSB (7-bit each) into a 14-bit value
* @param {number} msb - Most significant byte (0-127)
* @param {number} lsb - Least significant byte (0-127)
* @returns {number} 14-bit value (0-16383)
*/
export function decode14BitValue(msb, lsb) {
return (clamp(msb, 0, 127) << 7) | clamp(lsb, 0, 127)
}
/**
* Normalize a 14-bit value from input range
* @param {number} value - Input value
* @param {number} inputMin - Input minimum
* @param {number} inputMax - Input maximum
* @param {boolean} [invert=false] - Invert the output
* @returns {{msb: number, lsb: number}} MSB and LSB values
*/
export function normalize14BitValue(value, inputMin, inputMax, invert = false) {
const normalized = (value - inputMin) / (inputMax - inputMin)
const final = invert ? 1 - normalized : normalized
const value14Bit = final * 16383
return encode14BitValue(value14Bit)
}
/**
* Denormalize a 14-bit value to a custom range
* @param {number} msb - Most significant byte (0-127)
* @param {number} lsb - Least significant byte (0-127)
* @param {number} outputMin - Output minimum
* @param {number} outputMax - Output maximum
* @param {boolean} [invert=false] - Invert the input
* @returns {number} Denormalized value
*/
export function denormalize14BitValue(msb, lsb, outputMin, outputMax, invert = false) {
const value14Bit = decode14BitValue(msb, lsb)
let normalized = value14Bit / 16383
if (invert) {
normalized = 1 - normalized
}
return outputMin + normalized * (outputMax - outputMin)
}