@tremolo-ui/functions 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -356,6 +356,26 @@ function noteToFrequency(note, detune = 0, a4 = 440) {
356
356
  return a4 / 32 * 2 ** ((n - 9 + detune / 100) / 12);
357
357
  }
358
358
  /**
359
+ * Centre of the 14-bit pitch bend range: no bend.
360
+ *
361
+ * The range is not symmetric — 0 is 8192 below centre and 16383 is 8191 above
362
+ * — so a wheel at rest reports exactly this rather than half of the maximum.
363
+ */
364
+ const PITCH_BEND_CENTER = 8192;
365
+ /**
366
+ * A 14-bit pitch bend (0-16383) as -1 to 1, with 0 at rest.
367
+ *
368
+ * Each side of the centre is divided by its own length, since there is one
369
+ * more step below it than above: 0 gives -1 and 16383 gives 1 exactly.
370
+ * Dividing both sides by 8192 would never quite reach 1.
371
+ *
372
+ * @param value pitch bend: 0 ~ 16383
373
+ */
374
+ function normalizePitchBend(value) {
375
+ const offset = value - PITCH_BEND_CENTER;
376
+ return offset / (offset < 0 ? PITCH_BEND_CENTER : 8191);
377
+ }
378
+ /**
359
379
  * Semitones above the root, for each supported scale.
360
380
  *
361
381
  * Every entry starts at 0 and stays inside one octave, so a scale is a set of
@@ -666,6 +686,7 @@ function unitFormat(unit, options = {}) {
666
686
  };
667
687
  }
668
688
  //#endregion
689
+ exports.PITCH_BEND_CENTER = PITCH_BEND_CENTER;
669
690
  exports.clamp = clamp;
670
691
  exports.curveScale = curveScale;
671
692
  exports.curveWithCenterValue = curveWithCenterValue;
@@ -678,6 +699,7 @@ exports.isBlackKey = isBlackKey;
678
699
  exports.isWhiteKey = isWhiteKey;
679
700
  exports.linearScale = linearScale;
680
701
  exports.mapValue = mapValue;
702
+ exports.normalizePitchBend = normalizePitchBend;
681
703
  exports.normalizeValue = normalizeValue;
682
704
  exports.noteKey = noteKey;
683
705
  exports.noteKeys = noteKeys;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../src/math.ts","../src/scales.ts","../src/midi.ts","../src/unit.ts"],"sourcesContent":["/**\n * clamp value between min and max\n */\nexport function clamp(value: number, min: number, max: number) {\n return Math.max(min, Math.min(value, max))\n}\n\n/**\n * Normalize the value from 0 to 1, spreading the range evenly.\n *\n * This is the linear mapping and takes no curve of its own; a `Scale` builds\n * whatever curve it needs on top of it.\n */\nexport function normalizeValue(value: number, min: number, max: number) {\n if (min >= max) throw new RangeError('requirements: min < max')\n return clamp((value - min) / (max - min), 0, 1)\n}\n\n/**\n * Convert normalized values back to raw values, spreading the range evenly.\n *\n * The inverse of {@link normalizeValue}.\n */\nexport function rawValue(normalizedValue: number, min: number, max: number) {\n if (min >= max) throw new RangeError('requirements: min < max')\n return min + clamp(normalizedValue, 0, 1) * (max - min)\n}\n\n/**\n * Put a value on the grid the caller asked for, rounding a half step upwards.\n *\n * The rounding is done on the quotient rather than by comparing the distance\n * to the two neighbours, because both of those carry error of their own. The\n * quotient is cleared of its artefact first: `0.15 / 0.1` is 1.4999999999999998,\n * and a value sitting exactly on a half step would otherwise fall to whichever\n * side the last bit happened to land on — 0.25 rounded up while 0.15 and 0.35\n * rounded down.\n */\nexport function stepValue(value: number, step: number) {\n if (step <= 0) throw new RangeError('requirements: step > 0')\n const quotient = Math.round(toPrecision(value / step))\n // The product has its own debris to drop: 3141593 * 1e-6 is not 3.141593.\n const stepped = toPrecision(quotient * step)\n // Math.round answers -0 for anything in [-0.5, 0), and that reaches the\n // caller's onChange as a value that prints as 0 but is not it.\n return stepped === 0 ? 0 : stepped\n}\n\nexport function toFixed(x: number, fractionDigits?: number) {\n return Number(x.toFixed(fractionDigits))\n}\n\n/**\n * The significant decimal digits a double actually carries. A double holds a\n * little under 16, so anything past this is the binary representation showing\n * through rather than information.\n */\nconst SIGNIFICANT_DIGITS = 15\n\n/**\n * Drop the binary artefact from a computed value.\n *\n * Arithmetic on doubles leaves debris in the last couple of digits, and it\n * accumulates: adding 0.1 to 5 twelve times gives 5.699999999999998 rather\n * than 5.7, and the display of a control shows exactly that. Rounding to the\n * digits a double can carry removes it, and adds nothing back — the value was\n * already the result of a calculation whose own error is that size or larger.\n *\n * This is not the same as rounding to a `step`. {@link stepValue} puts a value\n * on a grid the caller asked for and is a decision about the value; this only\n * removes what was never in the value to begin with.\n *\n * @param significantDigits how many digits to keep. The default is the only\n * one that is purely artefact removal; a smaller number starts discarding real\n * precision.\n *\n * @example\n * toPrecision(5.1 + 0.1) // 5.2, rather than 5.199999999999999\n */\nexport function toPrecision(x: number, significantDigits = SIGNIFICANT_DIGITS) {\n // Zero has no significant digits to round to, and a non-finite value has no\n // decimal form to parse back.\n if (x === 0 || !Number.isFinite(x)) return x\n const rounded = Number(x.toPrecision(significantDigits))\n // Rounding up at the very top of the range overflows to Infinity, which is\n // a worse answer than the artefact.\n return Number.isFinite(rounded) ? rounded : x\n}\n\nexport function radian(degree: number) {\n return (Math.PI * degree) / 180\n}\n\nexport function degree(radian: number) {\n return (180 * radian) / Math.PI\n}\n\nexport function mapValue(\n value: number,\n inMin: number,\n inMax: number,\n outMin: number,\n outMax: number,\n) {\n return ((value - inMin) / (inMax - inMin)) * (outMax - outMin) + outMin\n}\n\nexport function dbToGain(db: number) {\n return Math.pow(10, db / 20)\n}\n\nexport function gainToDb(gain: number) {\n return 20 * (Math.log(gain) / Math.LN10)\n}\n","import { clamp, normalizeValue, rawValue } from './math'\n\n/**\n * How a value is distributed across the travel of a control.\n *\n * `normalize` and `denormalize` are inverses of each other: the position is\n * 0 at `min` and 1 at `max`, and everything in between is up to the scale.\n *\n * `min` and `max` are arguments rather than baked into the scale, so a scale\n * holds no state and can be a module level constant. Passing the same object\n * on every render therefore costs nothing.\n *\n * @example\n * ```ts\n * exponentialScale.denormalize(0.5, 20, 20000) // 632.45…\n * ```\n */\nexport interface Scale {\n /** Value to its position on the travel, 0-1. */\n normalize: (value: number, min: number, max: number) => number\n /** Position on the travel, 0-1, back to a value. */\n denormalize: (position: number, min: number, max: number) => number\n}\n\nfunction assertRange(min: number, max: number) {\n if (min >= max) throw new RangeError('requirements: min < max')\n}\n\nfunction assertPositiveFinite(value: number, name: string) {\n if (!Number.isFinite(value) || value <= 0) {\n throw new RangeError(`${name}: requirements: finite and greater than 0`)\n }\n}\n\n/**\n * Equal travel gives an equal change in value.\n *\n * The right default for anything already linear in perception: dB values,\n * pan, percentages, MIDI note numbers, semitones.\n */\nexport const linearScale: Scale = {\n normalize: (value, min, max) => normalizeValue(value, min, max),\n denormalize: (position, min, max) => rawValue(position, min, max),\n}\n\n/**\n * The power law of JUCE's `NormalisableRange::skew`, applied to `value - min`.\n *\n * Use it when the value has to agree with a JUCE or iPlug2 parameter — a\n * plugin UI in a WebView, say, where the knob must sit exactly where the\n * host's automation curve puts it. {@link skewWithCenterValue} gives the\n * factor that places a chosen value at the middle of the travel.\n *\n * `skew < 1` gives the lower end more travel, `skew > 1` the upper end.\n *\n * For new designs prefer {@link exponentialScale} or {@link curveScale}: the\n * slope of this curve is either zero or infinite at `min`, so the bottom of\n * the range is a dead zone or jumps.\n *\n * @param skew the JUCE skew factor\n */\nexport function skewScale(skew: number): Scale {\n assertPositiveFinite(skew, 'skewScale')\n return {\n // The two expressions JUCE uses, kept verbatim so the numbers agree with\n // a NormalisableRange: pow() one way, exp(log()) the other.\n normalize: (value, min, max) =>\n Math.pow(normalizeValue(value, min, max), skew),\n denormalize: (position, min, max) =>\n rawValue(\n skew === 1\n ? position\n : Math.exp(Math.log(clamp(position, 0, 1)) / skew),\n min,\n max,\n ),\n }\n}\n\n/**\n * The skew factor for {@link skewScale} that puts `centerValue` at the middle\n * of the travel — JUCE's `NormalisableRange::setSkewForCentre`.\n */\nexport function skewWithCenterValue(\n centerValue: number,\n min: number,\n max: number,\n) {\n assertRange(min, max)\n if (!(min < centerValue && centerValue < max))\n throw new RangeError('requirements: min < centerValue < max')\n return Math.log(0.5) / Math.log((centerValue - min) / (max - min))\n}\n\n/**\n * Equal travel gives an equal *ratio*, so an octave — or a percentage — takes\n * the same distance wherever it falls.\n *\n * The scale for frequency (a filter cutoff over 20-20000 Hz), free running\n * rates, and delay times.\n *\n * Requires `min` and `max` to be non-zero and of the same sign, since no\n * ratio reaches zero or crosses it. Use {@link curveScale} for a range that\n * starts at 0.\n */\nexport const exponentialScale: Scale = {\n normalize: (value, min, max) => {\n assertExponentialRange(min, max)\n // The value is clamped before the logarithm, not after: outside the range\n // the ratio can be negative, and log() would give NaN rather than a\n // position to clamp.\n const start = Math.log(Math.abs(min))\n const end = Math.log(Math.abs(max))\n return clamp(\n (Math.log(Math.abs(clamp(value, min, max))) - start) / (end - start),\n 0,\n 1,\n )\n },\n denormalize: (position, min, max) => {\n assertExponentialRange(min, max)\n const start = Math.log(Math.abs(min))\n const end = Math.log(Math.abs(max))\n const magnitude = Math.exp(start + (end - start) * clamp(position, 0, 1))\n return Math.sign(min) * magnitude\n },\n}\n\nfunction assertExponentialRange(min: number, max: number) {\n assertRange(min, max)\n if (min === 0 || max === 0 || Math.sign(min) !== Math.sign(max)) {\n throw new RangeError(\n 'exponentialScale: requirements: min and max are non-zero and have the same sign',\n )\n }\n}\n\n/**\n * An exponential bend that still passes exactly through `min` and `max`, so\n * unlike {@link exponentialScale} it works on a range that starts at 0 or\n * crosses it, and unlike {@link skewScale} its slope is neither zero nor\n * infinite at either end.\n *\n * The general purpose taper, and the same family as the curve of an envelope\n * segment (SuperCollider's `CurveWarp`).\n *\n * - `curve > 0` gives the lower end more travel — envelope times from 0 ms,\n * delay times, anything that wants fine control near the bottom\n * - `curve < 0` gives the upper end more travel — a volume fader over\n * -60..+6 dB that should be precise around 0 dB\n * - near 0 it is indistinguishable from {@link linearScale}, and is treated\n * as linear to avoid dividing by zero\n *\n * {@link curveWithCenterValue} gives the curve that places a chosen value at\n * the middle of the travel.\n *\n * @param curve how hard the curve bends, and in which direction\n */\nexport function curveScale(curve: number): Scale {\n // Beyond this the flatter half of the curve no longer has enough distinct\n // double values for normalize and denormalize to remain inverses.\n if (!Number.isFinite(curve) || Math.abs(curve) > 32) {\n throw new RangeError(\n 'curveScale: requirements: finite curve from -32 to 32',\n )\n }\n // The two coefficients blow up as the curve flattens: `a` divides by\n // 1 - e^curve, which goes to 0.\n if (Math.abs(curve) < 0.001) return linearScale\n\n return {\n normalize: (value, min, max) => {\n assertRange(min, max)\n const proportion = clamp((value - min) / (max - min), 0, 1)\n if (proportion === 0 || proportion === 1) return proportion\n if (curve > 0) {\n return (\n 1 + Math.log(proportion + (1 - proportion) * Math.exp(-curve)) / curve\n )\n }\n return Math.log1p(proportion * Math.expm1(curve)) / curve\n },\n denormalize: (position, min, max) => {\n assertRange(min, max)\n const p = clamp(position, 0, 1)\n if (p === 0) return min\n if (p === 1) return max\n const proportion =\n curve > 0\n ? (Math.exp(curve * (p - 1)) * (1 - Math.exp(-curve * p))) /\n (1 - Math.exp(-curve))\n : Math.expm1(curve * p) / Math.expm1(curve)\n return min + (max - min) * proportion\n },\n }\n}\n\n/**\n * {@link skewScale} mirrored about the middle of the range, so both halves\n * bend the same way — JUCE's `symmetricSkew`.\n *\n * For a bipolar control whose centre matters: detune over -100..+100 cents,\n * or a bipolar filter envelope amount, where the fine adjustment is around 0\n * rather than at either end.\n *\n * `skew < 1` gives the middle more travel, `skew > 1` the two ends.\n *\n * @param skew the JUCE skew factor\n */\nexport function symmetricSkewScale(skew: number): Scale {\n assertPositiveFinite(skew, 'symmetricSkewScale')\n return {\n normalize: (value, min, max) => {\n assertRange(min, max)\n const proportion = clamp((value - min) / (max - min), 0, 1)\n if (skew === 1) return proportion\n const distanceFromMiddle = 2 * proportion - 1\n return (\n (1 +\n Math.pow(Math.abs(distanceFromMiddle), skew) *\n Math.sign(distanceFromMiddle)) /\n 2\n )\n },\n denormalize: (position, min, max) => {\n assertRange(min, max)\n const p = clamp(position, 0, 1)\n let distanceFromMiddle = 2 * p - 1\n if (skew !== 1 && distanceFromMiddle !== 0) {\n distanceFromMiddle =\n Math.pow(Math.abs(distanceFromMiddle), 1 / skew) *\n Math.sign(distanceFromMiddle)\n }\n return min + ((max - min) / 2) * (1 + distanceFromMiddle)\n },\n }\n}\n\n/**\n * The curve for {@link curveScale} that puts `centerValue` at the middle of\n * the travel — the counterpart of {@link skewWithCenterValue}.\n */\nexport function curveWithCenterValue(\n centerValue: number,\n min: number,\n max: number,\n) {\n assertRange(min, max)\n if (!(min < centerValue && centerValue < max)) {\n throw new RangeError('requirements: min < centerValue < max')\n }\n // value(0.5) - min = range / (1 + e^(curve / 2))\n const proportion = (centerValue - min) / (max - min)\n return 2 * Math.log(1 / proportion - 1)\n}\n\n/**\n * How a value is scaled: the range it lives in, how it is rounded, and how it\n * is distributed across the travel.\n *\n * `AxisOptions` of `@tremolo-ui/dom` extends this, so a drag and a\n * wheel / keyboard nudge run the same value pipeline.\n */\nexport interface ValueRange {\n min: number\n max: number\n /**\n * Rounding applied to the value. Left unrounded when omitted.\n */\n step?: number\n /**\n * How the value is distributed across the travel.\n *\n * @default linearScale\n */\n scale?: Scale\n}\n","/**\n * Remainder that follows the sign of the divisor, rather than of the dividend.\n *\n * `%` in JavaScript keeps the sign of the left operand, so `-13 % 12` is `-1`\n * and cannot index a 12-element table. Every note here is placed by its\n * position within the octave, and note numbers below C-1 are negative.\n */\nfunction mod(n: number, m: number) {\n return ((n % m) + m) % m\n}\n\nexport const whiteKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] as const\n\nexport type WhiteKey = (typeof whiteKeys)[number]\n\nexport const noteKeys = [\n 'C',\n 'C#',\n 'D',\n 'D#',\n 'E',\n 'F',\n 'F#',\n 'G',\n 'G#',\n 'A',\n 'A#',\n 'B',\n] as const\n\nexport type NoteKey = (typeof noteKeys)[number]\n\nfunction assertSafeInteger(value: number, name: string) {\n if (!Number.isSafeInteger(value)) {\n throw new RangeError(`${name}: requirements: a safe integer`)\n }\n}\n\nexport function parseNoteName(noteName: string) {\n const m = noteName.match(/^([a-g])(#{0,2}|b{0,2})(-?\\d+)$/i)\n if (!m) throw new Error('Invalid note name')\n const [, letter, accidental, octave] = m\n const parsedOctave = Number(octave)\n assertSafeInteger(parsedOctave, 'octave')\n return {\n letter: letter.toLocaleUpperCase() as WhiteKey,\n accidental: accidental as '#' | '##' | 'b' | 'bb' | '',\n octave: parsedOctave,\n }\n}\n\n/**\n * Convert noteName to noteNumber\n */\nexport function noteNumber(noteName: string) {\n const { letter, accidental, octave } = parseNoteName(noteName)\n const noteIndex = noteKeys.indexOf(letter.toLocaleUpperCase() as NoteKey)\n const accidentalValue = (accidental[0] === 'b' ? -1 : 1) * accidental.length\n const result = noteIndex + 12 * (octave + 1) + accidentalValue\n assertSafeInteger(result, 'note number')\n return result\n}\n\n/**\n * Convert noteNumber to noteName\n * C-1 = 0\n * G9 = 127\n * @param noteNumber noteNumber\n */\nexport function noteName(noteNumber: number): `${NoteKey}${number}` {\n assertSafeInteger(noteNumber, 'note number')\n const noteIndex = mod(noteNumber, 12)\n const octave = Math.floor(noteNumber / 12) - 1\n return `${noteKeys[noteIndex]}${octave}`\n}\n\n/**\n * Convert noteNumber to noteKey\n */\nexport function noteKey(noteNumber: number): NoteKey {\n assertSafeInteger(noteNumber, 'note number')\n return noteKeys[mod(noteNumber, 12)]\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n */\nexport function isWhiteKey(note: number | string) {\n const n = typeof note === 'string' ? noteNumber(note) : note\n assertSafeInteger(n, 'note number')\n return (\n mod(n, 12) === 0 ||\n mod(n, 12) === 2 ||\n mod(n, 12) === 4 ||\n mod(n, 12) === 5 ||\n mod(n, 12) === 7 ||\n mod(n, 12) === 9 ||\n mod(n, 12) === 11\n )\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n */\nexport function isBlackKey(note: number | string) {\n return !isWhiteKey(note)\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n * @param detune [cent]\n * @param a4 A4 frequency [Hz]\n * @returns frequency [Hz]\n */\nexport function noteToFrequency(note: number | string, detune = 0, a4 = 440) {\n const n = typeof note === 'string' ? noteNumber(note) : note\n assertSafeInteger(n, 'note number')\n return (a4 / 32) * 2 ** ((n - 9 + detune / 100) / 12)\n}\n\n/**\n * Semitones above the root, for each supported scale.\n *\n * Every entry starts at 0 and stays inside one octave, so a scale is a set of\n * pitch classes rather than a set of notes: {@link inScale} compares against\n * it with the octave taken out.\n *\n * `ionian` and `aeolian` are the same sets as `major` and `naturalMinor`; both\n * spellings are here because both are what someone reaches for depending on\n * whether they are thinking in keys or in modes.\n */\nexport const scaleIntervals = {\n major: [0, 2, 4, 5, 7, 9, 11],\n naturalMinor: [0, 2, 3, 5, 7, 8, 10],\n harmonicMinor: [0, 2, 3, 5, 7, 8, 11],\n melodicMinor: [0, 2, 3, 5, 7, 9, 11],\n\n ionian: [0, 2, 4, 5, 7, 9, 11],\n dorian: [0, 2, 3, 5, 7, 9, 10],\n phrygian: [0, 1, 3, 5, 7, 8, 10],\n lydian: [0, 2, 4, 6, 7, 9, 11],\n mixolydian: [0, 2, 4, 5, 7, 9, 10],\n aeolian: [0, 2, 3, 5, 7, 8, 10],\n locrian: [0, 1, 3, 5, 6, 8, 10],\n\n majorPentatonic: [0, 2, 4, 7, 9],\n minorPentatonic: [0, 3, 5, 7, 10],\n blues: [0, 3, 5, 6, 7, 10],\n\n wholeTone: [0, 2, 4, 6, 8, 10],\n chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],\n} as const satisfies Record<string, readonly number[]>\n\nexport type ScaleName = keyof typeof scaleIntervals\n\n/**\n * Whether a note belongs to a scale, regardless of the octave either sits in.\n *\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n * @param root the note the scale is built on, in the same two forms\n *\n * @example\n * ```ts\n * inScale('F#4', 'D3', 'major') // true\n * ```\n */\nexport function inScale(\n note: number | string,\n root: number | string,\n name: ScaleName,\n): boolean {\n const n = typeof note === 'string' ? noteNumber(note) : note\n const r = typeof root === 'string' ? noteNumber(root) : root\n assertSafeInteger(n, 'note number')\n assertSafeInteger(r, 'root note number')\n return (scaleIntervals[name] as readonly number[]).includes(mod(n - r, 12))\n}\n\n/**\n * The notes of a scale, ascending from `root`.\n *\n * The octave above the root is not included: ask for more `octaves` instead, so\n * that concatenating the result of two calls does not repeat a note.\n *\n * @param root noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n * @param octaves how many octaves to cover\n *\n * @example\n * ```ts\n * scaleNotes('C3', 'majorPentatonic') // [48, 50, 52, 55, 57]\n * ```\n */\nexport function scaleNotes(\n root: number | string,\n name: ScaleName,\n octaves = 1,\n): number[] {\n const r = typeof root === 'string' ? noteNumber(root) : root\n assertSafeInteger(r, 'root note number')\n if (!Number.isSafeInteger(octaves) || octaves < 0) {\n throw new RangeError('octaves: requirements: a non-negative safe integer')\n }\n const intervals = scaleIntervals[name] as readonly number[]\n return Array.from({ length: octaves }, (_, octave) =>\n intervals.map((interval) => r + octave * 12 + interval),\n ).flat()\n}\n","import { toPrecision } from './math'\n\n/**\n * The SI prefixes {@link unitFormat} chooses between.\n *\n * Deliberately narrower than the full SI set: yocto through yotta are of no\n * use to an audio control, and every extra prefix is one more symbol `parse`\n * has to tell apart from a unit.\n */\nexport type SIPrefix = 'p' | 'n' | 'µ' | 'm' | '' | 'k' | 'M' | 'G'\n\n/** Ordered small to large. The empty symbol is the base unit. */\nconst PREFIXES: readonly [SIPrefix, number][] = [\n ['p', 1e-12],\n ['n', 1e-9],\n ['µ', 1e-6],\n ['m', 1e-3],\n ['', 1],\n ['k', 1e3],\n ['M', 1e6],\n ['G', 1e9],\n]\n\nconst PREFIX_SCALE = new Map<string, number>(PREFIXES)\n\n/**\n * Micro is written three ways. `µ` (U+00B5 MICRO SIGN) is what `format`\n * writes and what d3-format uses, `μ` (U+03BC GREEK SMALL LETTER MU) looks\n * identical and is what a Greek keyboard produces, and `u` is what everyone\n * actually types. All three read back the same.\n */\nconst MICRO_ALIASES: Record<string, SIPrefix> = { μ: 'µ', u: 'µ' }\n\nexport interface UnitFormatOptions {\n /**\n * The prefix the stored value is already in.\n *\n * A control that keeps milliseconds in `value` is `{ base: 'm' }` with a\n * unit of `'s'`: 1500 then displays as `1.5s`, and `parse` gives 1500 back.\n *\n * @default ''\n */\n base?: SIPrefix\n /**\n * Whether to scale the number and pick a prefix at all.\n *\n * Turn it off for anything that is not an SI quantity. dB, %, cents and\n * semitones do not take prefixes, and `-6dB` read as \"-6 deci-B\" is wrong\n * rather than merely unusual.\n *\n * @default true\n */\n prefixes?: boolean\n /**\n * Digits after the decimal point. The number is left as-is when omitted.\n */\n digits?: number\n /**\n * Text placed between the number and the unit.\n * @default ''\n */\n separator?: string\n}\n\n/** The `format` / `parse` pair a `NumberInput` takes. */\nexport interface UnitFormatter {\n format: (value: number) => string\n parse: (text: string) => number\n}\n\n/**\n * Divide by a prefix scale without showing the result of doing so in binary.\n *\n * `0.0005 / 1e-6` is 500.00000000000006, and with no `digits` to round it that\n * lands in the input as written.\n */\nfunction scaleBy(value: number, scale: number): number {\n return toPrecision(value / scale)\n}\n\n/** A number, then whatever followed it. */\nconst NUMBER_THEN_REST =\n /^([+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?)\\s*(.*)$/\n\n/**\n * Build the `format` and `parse` of a unit, as one pair.\n *\n * They are returned together because they have to agree: a `format` that\n * writes `1.23kHz` is only useful next to a `parse` that reads it back as\n * 1230. Spread the result into the input.\n *\n * @example\n * unitFormat('Hz') // 1234 -> '1.23kHz'\n * unitFormat('s', { base: 'm' }) // value in ms. 1500 -> '1.5s'\n * unitFormat('s', { base: 'm', digits: 2 }) // 1500 -> '1.50s'\n * unitFormat('dB', { prefixes: false, digits: 1 }) // -6.25 -> '-6.3dB'\n *\n * @example\n * <NumberInput.Root {...unitFormat('Hz', { digits: 2 })} value={v} onChange={setV}>\n */\nexport function unitFormat(\n unit: string,\n options: UnitFormatOptions = {},\n): UnitFormatter {\n const { base = '', prefixes = true, digits, separator = '' } = options\n if (unit === '' && base !== '') {\n throw new RangeError('unitFormat: base requires a non-empty unit')\n }\n const baseScale = PREFIX_SCALE.get(base) ?? 1\n\n /**\n * `toFixed` renders anything that rounds to zero from below as `-0`, which\n * is never what a control should show.\n */\n const fixed = (value: number) => {\n const text = digits !== undefined ? value.toFixed(digits) : String(value)\n return Number(text) === 0 ? text.replace('-', '') : text\n }\n\n if (!prefixes) {\n // The stored value goes out untouched, so the symbol has to name the unit\n // it is already in.\n const symbol = base + unit\n return {\n format: (value) =>\n Number.isFinite(value)\n ? fixed(value) + separator + symbol\n : String(value),\n // Nothing after the number can change the scale, so it is all ignored:\n // the number in front is the value, half-typed or not.\n parse: (text) => {\n const match = text.trim().match(NUMBER_THEN_REST)\n if (!match) return NaN\n const value = Number(match[1])\n return Number.isFinite(value) ? value : NaN\n },\n }\n }\n\n return {\n format: (value) => {\n if (!Number.isFinite(value)) return String(value)\n const si = value * baseScale\n // Zero has no magnitude to read, so it stays in the base unit.\n let index = PREFIXES.findIndex(([, scale]) => scale === 1)\n if (si !== 0) {\n // The largest prefix that leaves at least one digit before the point.\n // Below the smallest prefix the number just gets small: `p` is the\n // floor, as `G` is the ceiling.\n const magnitude = Math.abs(si)\n index = 0\n for (let i = PREFIXES.length - 1; i >= 0; i--) {\n if (magnitude >= PREFIXES[i][1]) {\n index = i\n break\n }\n }\n }\n let text = fixed(scaleBy(si, PREFIXES[index][1]))\n // Rounding can carry the number up out of its own prefix — 999.99Hz at\n // one digit is 1000.0Hz, which should read 1.0kHz. One step is always\n // enough, since the carry is at most a factor of ten.\n if (Math.abs(Number(text)) >= 1000 && index < PREFIXES.length - 1) {\n index += 1\n text = fixed(scaleBy(si, PREFIXES[index][1]))\n }\n return text + separator + PREFIXES[index][0] + unit\n },\n\n parse: (text) => {\n const match = text.trim().match(NUMBER_THEN_REST)\n if (!match) return NaN\n const number = Number(match[1])\n if (!Number.isFinite(number)) return NaN\n\n let suffix = match[2].trim()\n const separatorText = separator.trim()\n if (separatorText !== '' && suffix.startsWith(separatorText)) {\n suffix = suffix.slice(separatorText.length).trim()\n }\n // A bare number is in the unit the value is stored in, which is what\n // the input shows once the format is stripped.\n if (suffix === '') return number\n\n // The unit symbol is matched first, so a unit that is itself a prefix\n // letter wins over the prefix reading: `5m` for a unit of `m` is five\n // metres, not five milli-.\n let prefix: string | null = null\n if (unit !== '' && suffix.endsWith(unit)) {\n prefix = suffix.slice(0, suffix.length - unit.length)\n } else if (suffix.length <= 1) {\n prefix = suffix\n }\n if (prefix === null) return number\n\n const normalized = MICRO_ALIASES[prefix] ?? prefix\n const scale = PREFIX_SCALE.get(normalized)\n // Unrecognised text after the number is ignored rather than rejected,\n // so that a half-typed entry still yields the number in front of it.\n if (scale === undefined) return number\n return (number * scale) / baseScale\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/math.ts","../src/scales.ts","../src/midi.ts","../src/unit.ts"],"sourcesContent":["/**\n * clamp value between min and max\n */\nexport function clamp(value: number, min: number, max: number) {\n return Math.max(min, Math.min(value, max))\n}\n\n/**\n * Normalize the value from 0 to 1, spreading the range evenly.\n *\n * This is the linear mapping and takes no curve of its own; a `Scale` builds\n * whatever curve it needs on top of it.\n */\nexport function normalizeValue(value: number, min: number, max: number) {\n if (min >= max) throw new RangeError('requirements: min < max')\n return clamp((value - min) / (max - min), 0, 1)\n}\n\n/**\n * Convert normalized values back to raw values, spreading the range evenly.\n *\n * The inverse of {@link normalizeValue}.\n */\nexport function rawValue(normalizedValue: number, min: number, max: number) {\n if (min >= max) throw new RangeError('requirements: min < max')\n return min + clamp(normalizedValue, 0, 1) * (max - min)\n}\n\n/**\n * Put a value on the grid the caller asked for, rounding a half step upwards.\n *\n * The rounding is done on the quotient rather than by comparing the distance\n * to the two neighbours, because both of those carry error of their own. The\n * quotient is cleared of its artefact first: `0.15 / 0.1` is 1.4999999999999998,\n * and a value sitting exactly on a half step would otherwise fall to whichever\n * side the last bit happened to land on — 0.25 rounded up while 0.15 and 0.35\n * rounded down.\n */\nexport function stepValue(value: number, step: number) {\n if (step <= 0) throw new RangeError('requirements: step > 0')\n const quotient = Math.round(toPrecision(value / step))\n // The product has its own debris to drop: 3141593 * 1e-6 is not 3.141593.\n const stepped = toPrecision(quotient * step)\n // Math.round answers -0 for anything in [-0.5, 0), and that reaches the\n // caller's onChange as a value that prints as 0 but is not it.\n return stepped === 0 ? 0 : stepped\n}\n\nexport function toFixed(x: number, fractionDigits?: number) {\n return Number(x.toFixed(fractionDigits))\n}\n\n/**\n * The significant decimal digits a double actually carries. A double holds a\n * little under 16, so anything past this is the binary representation showing\n * through rather than information.\n */\nconst SIGNIFICANT_DIGITS = 15\n\n/**\n * Drop the binary artefact from a computed value.\n *\n * Arithmetic on doubles leaves debris in the last couple of digits, and it\n * accumulates: adding 0.1 to 5 twelve times gives 5.699999999999998 rather\n * than 5.7, and the display of a control shows exactly that. Rounding to the\n * digits a double can carry removes it, and adds nothing back — the value was\n * already the result of a calculation whose own error is that size or larger.\n *\n * This is not the same as rounding to a `step`. {@link stepValue} puts a value\n * on a grid the caller asked for and is a decision about the value; this only\n * removes what was never in the value to begin with.\n *\n * @param significantDigits how many digits to keep. The default is the only\n * one that is purely artefact removal; a smaller number starts discarding real\n * precision.\n *\n * @example\n * toPrecision(5.1 + 0.1) // 5.2, rather than 5.199999999999999\n */\nexport function toPrecision(x: number, significantDigits = SIGNIFICANT_DIGITS) {\n // Zero has no significant digits to round to, and a non-finite value has no\n // decimal form to parse back.\n if (x === 0 || !Number.isFinite(x)) return x\n const rounded = Number(x.toPrecision(significantDigits))\n // Rounding up at the very top of the range overflows to Infinity, which is\n // a worse answer than the artefact.\n return Number.isFinite(rounded) ? rounded : x\n}\n\nexport function radian(degree: number) {\n return (Math.PI * degree) / 180\n}\n\nexport function degree(radian: number) {\n return (180 * radian) / Math.PI\n}\n\nexport function mapValue(\n value: number,\n inMin: number,\n inMax: number,\n outMin: number,\n outMax: number,\n) {\n return ((value - inMin) / (inMax - inMin)) * (outMax - outMin) + outMin\n}\n\nexport function dbToGain(db: number) {\n return Math.pow(10, db / 20)\n}\n\nexport function gainToDb(gain: number) {\n return 20 * (Math.log(gain) / Math.LN10)\n}\n","import { clamp, normalizeValue, rawValue } from './math'\n\n/**\n * How a value is distributed across the travel of a control.\n *\n * `normalize` and `denormalize` are inverses of each other: the position is\n * 0 at `min` and 1 at `max`, and everything in between is up to the scale.\n *\n * `min` and `max` are arguments rather than baked into the scale, so a scale\n * holds no state and can be a module level constant. Passing the same object\n * on every render therefore costs nothing.\n *\n * @example\n * ```ts\n * exponentialScale.denormalize(0.5, 20, 20000) // 632.45…\n * ```\n */\nexport interface Scale {\n /** Value to its position on the travel, 0-1. */\n normalize: (value: number, min: number, max: number) => number\n /** Position on the travel, 0-1, back to a value. */\n denormalize: (position: number, min: number, max: number) => number\n}\n\nfunction assertRange(min: number, max: number) {\n if (min >= max) throw new RangeError('requirements: min < max')\n}\n\nfunction assertPositiveFinite(value: number, name: string) {\n if (!Number.isFinite(value) || value <= 0) {\n throw new RangeError(`${name}: requirements: finite and greater than 0`)\n }\n}\n\n/**\n * Equal travel gives an equal change in value.\n *\n * The right default for anything already linear in perception: dB values,\n * pan, percentages, MIDI note numbers, semitones.\n */\nexport const linearScale: Scale = {\n normalize: (value, min, max) => normalizeValue(value, min, max),\n denormalize: (position, min, max) => rawValue(position, min, max),\n}\n\n/**\n * The power law of JUCE's `NormalisableRange::skew`, applied to `value - min`.\n *\n * Use it when the value has to agree with a JUCE or iPlug2 parameter — a\n * plugin UI in a WebView, say, where the knob must sit exactly where the\n * host's automation curve puts it. {@link skewWithCenterValue} gives the\n * factor that places a chosen value at the middle of the travel.\n *\n * `skew < 1` gives the lower end more travel, `skew > 1` the upper end.\n *\n * For new designs prefer {@link exponentialScale} or {@link curveScale}: the\n * slope of this curve is either zero or infinite at `min`, so the bottom of\n * the range is a dead zone or jumps.\n *\n * @param skew the JUCE skew factor\n */\nexport function skewScale(skew: number): Scale {\n assertPositiveFinite(skew, 'skewScale')\n return {\n // The two expressions JUCE uses, kept verbatim so the numbers agree with\n // a NormalisableRange: pow() one way, exp(log()) the other.\n normalize: (value, min, max) =>\n Math.pow(normalizeValue(value, min, max), skew),\n denormalize: (position, min, max) =>\n rawValue(\n skew === 1\n ? position\n : Math.exp(Math.log(clamp(position, 0, 1)) / skew),\n min,\n max,\n ),\n }\n}\n\n/**\n * The skew factor for {@link skewScale} that puts `centerValue` at the middle\n * of the travel — JUCE's `NormalisableRange::setSkewForCentre`.\n */\nexport function skewWithCenterValue(\n centerValue: number,\n min: number,\n max: number,\n) {\n assertRange(min, max)\n if (!(min < centerValue && centerValue < max))\n throw new RangeError('requirements: min < centerValue < max')\n return Math.log(0.5) / Math.log((centerValue - min) / (max - min))\n}\n\n/**\n * Equal travel gives an equal *ratio*, so an octave — or a percentage — takes\n * the same distance wherever it falls.\n *\n * The scale for frequency (a filter cutoff over 20-20000 Hz), free running\n * rates, and delay times.\n *\n * Requires `min` and `max` to be non-zero and of the same sign, since no\n * ratio reaches zero or crosses it. Use {@link curveScale} for a range that\n * starts at 0.\n */\nexport const exponentialScale: Scale = {\n normalize: (value, min, max) => {\n assertExponentialRange(min, max)\n // The value is clamped before the logarithm, not after: outside the range\n // the ratio can be negative, and log() would give NaN rather than a\n // position to clamp.\n const start = Math.log(Math.abs(min))\n const end = Math.log(Math.abs(max))\n return clamp(\n (Math.log(Math.abs(clamp(value, min, max))) - start) / (end - start),\n 0,\n 1,\n )\n },\n denormalize: (position, min, max) => {\n assertExponentialRange(min, max)\n const start = Math.log(Math.abs(min))\n const end = Math.log(Math.abs(max))\n const magnitude = Math.exp(start + (end - start) * clamp(position, 0, 1))\n return Math.sign(min) * magnitude\n },\n}\n\nfunction assertExponentialRange(min: number, max: number) {\n assertRange(min, max)\n if (min === 0 || max === 0 || Math.sign(min) !== Math.sign(max)) {\n throw new RangeError(\n 'exponentialScale: requirements: min and max are non-zero and have the same sign',\n )\n }\n}\n\n/**\n * An exponential bend that still passes exactly through `min` and `max`, so\n * unlike {@link exponentialScale} it works on a range that starts at 0 or\n * crosses it, and unlike {@link skewScale} its slope is neither zero nor\n * infinite at either end.\n *\n * The general purpose taper, and the same family as the curve of an envelope\n * segment (SuperCollider's `CurveWarp`).\n *\n * - `curve > 0` gives the lower end more travel — envelope times from 0 ms,\n * delay times, anything that wants fine control near the bottom\n * - `curve < 0` gives the upper end more travel — a volume fader over\n * -60..+6 dB that should be precise around 0 dB\n * - near 0 it is indistinguishable from {@link linearScale}, and is treated\n * as linear to avoid dividing by zero\n *\n * {@link curveWithCenterValue} gives the curve that places a chosen value at\n * the middle of the travel.\n *\n * @param curve how hard the curve bends, and in which direction\n */\nexport function curveScale(curve: number): Scale {\n // Beyond this the flatter half of the curve no longer has enough distinct\n // double values for normalize and denormalize to remain inverses.\n if (!Number.isFinite(curve) || Math.abs(curve) > 32) {\n throw new RangeError(\n 'curveScale: requirements: finite curve from -32 to 32',\n )\n }\n // The two coefficients blow up as the curve flattens: `a` divides by\n // 1 - e^curve, which goes to 0.\n if (Math.abs(curve) < 0.001) return linearScale\n\n return {\n normalize: (value, min, max) => {\n assertRange(min, max)\n const proportion = clamp((value - min) / (max - min), 0, 1)\n if (proportion === 0 || proportion === 1) return proportion\n if (curve > 0) {\n return (\n 1 + Math.log(proportion + (1 - proportion) * Math.exp(-curve)) / curve\n )\n }\n return Math.log1p(proportion * Math.expm1(curve)) / curve\n },\n denormalize: (position, min, max) => {\n assertRange(min, max)\n const p = clamp(position, 0, 1)\n if (p === 0) return min\n if (p === 1) return max\n const proportion =\n curve > 0\n ? (Math.exp(curve * (p - 1)) * (1 - Math.exp(-curve * p))) /\n (1 - Math.exp(-curve))\n : Math.expm1(curve * p) / Math.expm1(curve)\n return min + (max - min) * proportion\n },\n }\n}\n\n/**\n * {@link skewScale} mirrored about the middle of the range, so both halves\n * bend the same way — JUCE's `symmetricSkew`.\n *\n * For a bipolar control whose centre matters: detune over -100..+100 cents,\n * or a bipolar filter envelope amount, where the fine adjustment is around 0\n * rather than at either end.\n *\n * `skew < 1` gives the middle more travel, `skew > 1` the two ends.\n *\n * @param skew the JUCE skew factor\n */\nexport function symmetricSkewScale(skew: number): Scale {\n assertPositiveFinite(skew, 'symmetricSkewScale')\n return {\n normalize: (value, min, max) => {\n assertRange(min, max)\n const proportion = clamp((value - min) / (max - min), 0, 1)\n if (skew === 1) return proportion\n const distanceFromMiddle = 2 * proportion - 1\n return (\n (1 +\n Math.pow(Math.abs(distanceFromMiddle), skew) *\n Math.sign(distanceFromMiddle)) /\n 2\n )\n },\n denormalize: (position, min, max) => {\n assertRange(min, max)\n const p = clamp(position, 0, 1)\n let distanceFromMiddle = 2 * p - 1\n if (skew !== 1 && distanceFromMiddle !== 0) {\n distanceFromMiddle =\n Math.pow(Math.abs(distanceFromMiddle), 1 / skew) *\n Math.sign(distanceFromMiddle)\n }\n return min + ((max - min) / 2) * (1 + distanceFromMiddle)\n },\n }\n}\n\n/**\n * The curve for {@link curveScale} that puts `centerValue` at the middle of\n * the travel — the counterpart of {@link skewWithCenterValue}.\n */\nexport function curveWithCenterValue(\n centerValue: number,\n min: number,\n max: number,\n) {\n assertRange(min, max)\n if (!(min < centerValue && centerValue < max)) {\n throw new RangeError('requirements: min < centerValue < max')\n }\n // value(0.5) - min = range / (1 + e^(curve / 2))\n const proportion = (centerValue - min) / (max - min)\n return 2 * Math.log(1 / proportion - 1)\n}\n\n/**\n * How a value is scaled: the range it lives in, how it is rounded, and how it\n * is distributed across the travel.\n *\n * `AxisOptions` of `@tremolo-ui/dom` extends this, so a drag and a\n * wheel / keyboard nudge run the same value pipeline.\n */\nexport interface ValueRange {\n min: number\n max: number\n /**\n * Rounding applied to the value. Left unrounded when omitted.\n */\n step?: number\n /**\n * How the value is distributed across the travel.\n *\n * @default linearScale\n */\n scale?: Scale\n}\n","/**\n * Remainder that follows the sign of the divisor, rather than of the dividend.\n *\n * `%` in JavaScript keeps the sign of the left operand, so `-13 % 12` is `-1`\n * and cannot index a 12-element table. Every note here is placed by its\n * position within the octave, and note numbers below C-1 are negative.\n */\nfunction mod(n: number, m: number) {\n return ((n % m) + m) % m\n}\n\nexport const whiteKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] as const\n\nexport type WhiteKey = (typeof whiteKeys)[number]\n\nexport const noteKeys = [\n 'C',\n 'C#',\n 'D',\n 'D#',\n 'E',\n 'F',\n 'F#',\n 'G',\n 'G#',\n 'A',\n 'A#',\n 'B',\n] as const\n\nexport type NoteKey = (typeof noteKeys)[number]\n\nfunction assertSafeInteger(value: number, name: string) {\n if (!Number.isSafeInteger(value)) {\n throw new RangeError(`${name}: requirements: a safe integer`)\n }\n}\n\nexport function parseNoteName(noteName: string) {\n const m = noteName.match(/^([a-g])(#{0,2}|b{0,2})(-?\\d+)$/i)\n if (!m) throw new Error('Invalid note name')\n const [, letter, accidental, octave] = m\n const parsedOctave = Number(octave)\n assertSafeInteger(parsedOctave, 'octave')\n return {\n letter: letter.toLocaleUpperCase() as WhiteKey,\n accidental: accidental as '#' | '##' | 'b' | 'bb' | '',\n octave: parsedOctave,\n }\n}\n\n/**\n * Convert noteName to noteNumber\n */\nexport function noteNumber(noteName: string) {\n const { letter, accidental, octave } = parseNoteName(noteName)\n const noteIndex = noteKeys.indexOf(letter.toLocaleUpperCase() as NoteKey)\n const accidentalValue = (accidental[0] === 'b' ? -1 : 1) * accidental.length\n const result = noteIndex + 12 * (octave + 1) + accidentalValue\n assertSafeInteger(result, 'note number')\n return result\n}\n\n/**\n * Convert noteNumber to noteName\n * C-1 = 0\n * G9 = 127\n * @param noteNumber noteNumber\n */\nexport function noteName(noteNumber: number): `${NoteKey}${number}` {\n assertSafeInteger(noteNumber, 'note number')\n const noteIndex = mod(noteNumber, 12)\n const octave = Math.floor(noteNumber / 12) - 1\n return `${noteKeys[noteIndex]}${octave}`\n}\n\n/**\n * Convert noteNumber to noteKey\n */\nexport function noteKey(noteNumber: number): NoteKey {\n assertSafeInteger(noteNumber, 'note number')\n return noteKeys[mod(noteNumber, 12)]\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n */\nexport function isWhiteKey(note: number | string) {\n const n = typeof note === 'string' ? noteNumber(note) : note\n assertSafeInteger(n, 'note number')\n return (\n mod(n, 12) === 0 ||\n mod(n, 12) === 2 ||\n mod(n, 12) === 4 ||\n mod(n, 12) === 5 ||\n mod(n, 12) === 7 ||\n mod(n, 12) === 9 ||\n mod(n, 12) === 11\n )\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n */\nexport function isBlackKey(note: number | string) {\n return !isWhiteKey(note)\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n * @param detune [cent]\n * @param a4 A4 frequency [Hz]\n * @returns frequency [Hz]\n */\nexport function noteToFrequency(note: number | string, detune = 0, a4 = 440) {\n const n = typeof note === 'string' ? noteNumber(note) : note\n assertSafeInteger(n, 'note number')\n return (a4 / 32) * 2 ** ((n - 9 + detune / 100) / 12)\n}\n\n/**\n * Centre of the 14-bit pitch bend range: no bend.\n *\n * The range is not symmetric — 0 is 8192 below centre and 16383 is 8191 above\n * — so a wheel at rest reports exactly this rather than half of the maximum.\n */\nexport const PITCH_BEND_CENTER = 8192\n\n/**\n * A 14-bit pitch bend (0-16383) as -1 to 1, with 0 at rest.\n *\n * Each side of the centre is divided by its own length, since there is one\n * more step below it than above: 0 gives -1 and 16383 gives 1 exactly.\n * Dividing both sides by 8192 would never quite reach 1.\n *\n * @param value pitch bend: 0 ~ 16383\n */\nexport function normalizePitchBend(value: number) {\n const offset = value - PITCH_BEND_CENTER\n return offset / (offset < 0 ? PITCH_BEND_CENTER : PITCH_BEND_CENTER - 1)\n}\n\n/**\n * Semitones above the root, for each supported scale.\n *\n * Every entry starts at 0 and stays inside one octave, so a scale is a set of\n * pitch classes rather than a set of notes: {@link inScale} compares against\n * it with the octave taken out.\n *\n * `ionian` and `aeolian` are the same sets as `major` and `naturalMinor`; both\n * spellings are here because both are what someone reaches for depending on\n * whether they are thinking in keys or in modes.\n */\nexport const scaleIntervals = {\n major: [0, 2, 4, 5, 7, 9, 11],\n naturalMinor: [0, 2, 3, 5, 7, 8, 10],\n harmonicMinor: [0, 2, 3, 5, 7, 8, 11],\n melodicMinor: [0, 2, 3, 5, 7, 9, 11],\n\n ionian: [0, 2, 4, 5, 7, 9, 11],\n dorian: [0, 2, 3, 5, 7, 9, 10],\n phrygian: [0, 1, 3, 5, 7, 8, 10],\n lydian: [0, 2, 4, 6, 7, 9, 11],\n mixolydian: [0, 2, 4, 5, 7, 9, 10],\n aeolian: [0, 2, 3, 5, 7, 8, 10],\n locrian: [0, 1, 3, 5, 6, 8, 10],\n\n majorPentatonic: [0, 2, 4, 7, 9],\n minorPentatonic: [0, 3, 5, 7, 10],\n blues: [0, 3, 5, 6, 7, 10],\n\n wholeTone: [0, 2, 4, 6, 8, 10],\n chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],\n} as const satisfies Record<string, readonly number[]>\n\nexport type ScaleName = keyof typeof scaleIntervals\n\n/**\n * Whether a note belongs to a scale, regardless of the octave either sits in.\n *\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n * @param root the note the scale is built on, in the same two forms\n *\n * @example\n * ```ts\n * inScale('F#4', 'D3', 'major') // true\n * ```\n */\nexport function inScale(\n note: number | string,\n root: number | string,\n name: ScaleName,\n): boolean {\n const n = typeof note === 'string' ? noteNumber(note) : note\n const r = typeof root === 'string' ? noteNumber(root) : root\n assertSafeInteger(n, 'note number')\n assertSafeInteger(r, 'root note number')\n return (scaleIntervals[name] as readonly number[]).includes(mod(n - r, 12))\n}\n\n/**\n * The notes of a scale, ascending from `root`.\n *\n * The octave above the root is not included: ask for more `octaves` instead, so\n * that concatenating the result of two calls does not repeat a note.\n *\n * @param root noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n * @param octaves how many octaves to cover\n *\n * @example\n * ```ts\n * scaleNotes('C3', 'majorPentatonic') // [48, 50, 52, 55, 57]\n * ```\n */\nexport function scaleNotes(\n root: number | string,\n name: ScaleName,\n octaves = 1,\n): number[] {\n const r = typeof root === 'string' ? noteNumber(root) : root\n assertSafeInteger(r, 'root note number')\n if (!Number.isSafeInteger(octaves) || octaves < 0) {\n throw new RangeError('octaves: requirements: a non-negative safe integer')\n }\n const intervals = scaleIntervals[name] as readonly number[]\n return Array.from({ length: octaves }, (_, octave) =>\n intervals.map((interval) => r + octave * 12 + interval),\n ).flat()\n}\n","import { toPrecision } from './math'\n\n/**\n * The SI prefixes {@link unitFormat} chooses between.\n *\n * Deliberately narrower than the full SI set: yocto through yotta are of no\n * use to an audio control, and every extra prefix is one more symbol `parse`\n * has to tell apart from a unit.\n */\nexport type SIPrefix = 'p' | 'n' | 'µ' | 'm' | '' | 'k' | 'M' | 'G'\n\n/** Ordered small to large. The empty symbol is the base unit. */\nconst PREFIXES: readonly [SIPrefix, number][] = [\n ['p', 1e-12],\n ['n', 1e-9],\n ['µ', 1e-6],\n ['m', 1e-3],\n ['', 1],\n ['k', 1e3],\n ['M', 1e6],\n ['G', 1e9],\n]\n\nconst PREFIX_SCALE = new Map<string, number>(PREFIXES)\n\n/**\n * Micro is written three ways. `µ` (U+00B5 MICRO SIGN) is what `format`\n * writes and what d3-format uses, `μ` (U+03BC GREEK SMALL LETTER MU) looks\n * identical and is what a Greek keyboard produces, and `u` is what everyone\n * actually types. All three read back the same.\n */\nconst MICRO_ALIASES: Record<string, SIPrefix> = { μ: 'µ', u: 'µ' }\n\nexport interface UnitFormatOptions {\n /**\n * The prefix the stored value is already in.\n *\n * A control that keeps milliseconds in `value` is `{ base: 'm' }` with a\n * unit of `'s'`: 1500 then displays as `1.5s`, and `parse` gives 1500 back.\n *\n * @default ''\n */\n base?: SIPrefix\n /**\n * Whether to scale the number and pick a prefix at all.\n *\n * Turn it off for anything that is not an SI quantity. dB, %, cents and\n * semitones do not take prefixes, and `-6dB` read as \"-6 deci-B\" is wrong\n * rather than merely unusual.\n *\n * @default true\n */\n prefixes?: boolean\n /**\n * Digits after the decimal point. The number is left as-is when omitted.\n */\n digits?: number\n /**\n * Text placed between the number and the unit.\n * @default ''\n */\n separator?: string\n}\n\n/** The `format` / `parse` pair a `NumberInput` takes. */\nexport interface UnitFormatter {\n format: (value: number) => string\n parse: (text: string) => number\n}\n\n/**\n * Divide by a prefix scale without showing the result of doing so in binary.\n *\n * `0.0005 / 1e-6` is 500.00000000000006, and with no `digits` to round it that\n * lands in the input as written.\n */\nfunction scaleBy(value: number, scale: number): number {\n return toPrecision(value / scale)\n}\n\n/** A number, then whatever followed it. */\nconst NUMBER_THEN_REST =\n /^([+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?)\\s*(.*)$/\n\n/**\n * Build the `format` and `parse` of a unit, as one pair.\n *\n * They are returned together because they have to agree: a `format` that\n * writes `1.23kHz` is only useful next to a `parse` that reads it back as\n * 1230. Spread the result into the input.\n *\n * @example\n * unitFormat('Hz') // 1234 -> '1.23kHz'\n * unitFormat('s', { base: 'm' }) // value in ms. 1500 -> '1.5s'\n * unitFormat('s', { base: 'm', digits: 2 }) // 1500 -> '1.50s'\n * unitFormat('dB', { prefixes: false, digits: 1 }) // -6.25 -> '-6.3dB'\n *\n * @example\n * <NumberInput.Root {...unitFormat('Hz', { digits: 2 })} value={v} onChange={setV}>\n */\nexport function unitFormat(\n unit: string,\n options: UnitFormatOptions = {},\n): UnitFormatter {\n const { base = '', prefixes = true, digits, separator = '' } = options\n if (unit === '' && base !== '') {\n throw new RangeError('unitFormat: base requires a non-empty unit')\n }\n const baseScale = PREFIX_SCALE.get(base) ?? 1\n\n /**\n * `toFixed` renders anything that rounds to zero from below as `-0`, which\n * is never what a control should show.\n */\n const fixed = (value: number) => {\n const text = digits !== undefined ? value.toFixed(digits) : String(value)\n return Number(text) === 0 ? text.replace('-', '') : text\n }\n\n if (!prefixes) {\n // The stored value goes out untouched, so the symbol has to name the unit\n // it is already in.\n const symbol = base + unit\n return {\n format: (value) =>\n Number.isFinite(value)\n ? fixed(value) + separator + symbol\n : String(value),\n // Nothing after the number can change the scale, so it is all ignored:\n // the number in front is the value, half-typed or not.\n parse: (text) => {\n const match = text.trim().match(NUMBER_THEN_REST)\n if (!match) return NaN\n const value = Number(match[1])\n return Number.isFinite(value) ? value : NaN\n },\n }\n }\n\n return {\n format: (value) => {\n if (!Number.isFinite(value)) return String(value)\n const si = value * baseScale\n // Zero has no magnitude to read, so it stays in the base unit.\n let index = PREFIXES.findIndex(([, scale]) => scale === 1)\n if (si !== 0) {\n // The largest prefix that leaves at least one digit before the point.\n // Below the smallest prefix the number just gets small: `p` is the\n // floor, as `G` is the ceiling.\n const magnitude = Math.abs(si)\n index = 0\n for (let i = PREFIXES.length - 1; i >= 0; i--) {\n if (magnitude >= PREFIXES[i][1]) {\n index = i\n break\n }\n }\n }\n let text = fixed(scaleBy(si, PREFIXES[index][1]))\n // Rounding can carry the number up out of its own prefix — 999.99Hz at\n // one digit is 1000.0Hz, which should read 1.0kHz. One step is always\n // enough, since the carry is at most a factor of ten.\n if (Math.abs(Number(text)) >= 1000 && index < PREFIXES.length - 1) {\n index += 1\n text = fixed(scaleBy(si, PREFIXES[index][1]))\n }\n return text + separator + PREFIXES[index][0] + unit\n },\n\n parse: (text) => {\n const match = text.trim().match(NUMBER_THEN_REST)\n if (!match) return NaN\n const number = Number(match[1])\n if (!Number.isFinite(number)) return NaN\n\n let suffix = match[2].trim()\n const separatorText = separator.trim()\n if (separatorText !== '' && suffix.startsWith(separatorText)) {\n suffix = suffix.slice(separatorText.length).trim()\n }\n // A bare number is in the unit the value is stored in, which is what\n // the input shows once the format is stripped.\n if (suffix === '') return number\n\n // The unit symbol is matched first, so a unit that is itself a prefix\n // letter wins over the prefix reading: `5m` for a unit of `m` is five\n // metres, not five milli-.\n let prefix: string | null = null\n if (unit !== '' && suffix.endsWith(unit)) {\n prefix = suffix.slice(0, suffix.length - unit.length)\n } else if (suffix.length <= 1) {\n prefix = suffix\n }\n if (prefix === null) return number\n\n const normalized = MICRO_ALIASES[prefix] ?? prefix\n const scale = PREFIX_SCALE.get(normalized)\n // Unrecognised text after the number is ignored rather than rejected,\n // so that a half-typed entry still yields the number in front of it.\n if (scale === undefined) return number\n return (number * scale) / baseScale\n },\n }\n}\n"],"mappings":";;;;;AAGA,SAAgB,MAAM,OAAe,KAAa,KAAa;CAC7D,OAAO,KAAK,IAAI,KAAK,KAAK,IAAI,OAAO,GAAG,CAAC;AAC3C;;;;;;;AAQA,SAAgB,eAAe,OAAe,KAAa,KAAa;CACtE,IAAI,OAAO,KAAK,MAAM,IAAI,WAAW,yBAAyB;CAC9D,OAAO,OAAO,QAAQ,QAAQ,MAAM,MAAM,GAAG,CAAC;AAChD;;;;;;AAOA,SAAgB,SAAS,iBAAyB,KAAa,KAAa;CAC1E,IAAI,OAAO,KAAK,MAAM,IAAI,WAAW,yBAAyB;CAC9D,OAAO,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,MAAM;AACrD;;;;;;;;;;;AAYA,SAAgB,UAAU,OAAe,MAAc;CACrD,IAAI,QAAQ,GAAG,MAAM,IAAI,WAAW,wBAAwB;CAG5D,MAAM,UAAU,YAFC,KAAK,MAAM,YAAY,QAAQ,IAAI,CAEjB,IAAI,IAAI;CAG3C,OAAO,YAAY,IAAI,IAAI;AAC7B;AAEA,SAAgB,QAAQ,GAAW,gBAAyB;CAC1D,OAAO,OAAO,EAAE,QAAQ,cAAc,CAAC;AACzC;;;;;;AAOA,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;AAsB3B,SAAgB,YAAY,GAAW,oBAAoB,oBAAoB;CAG7E,IAAI,MAAM,KAAK,CAAC,OAAO,SAAS,CAAC,GAAG,OAAO;CAC3C,MAAM,UAAU,OAAO,EAAE,YAAY,iBAAiB,CAAC;CAGvD,OAAO,OAAO,SAAS,OAAO,IAAI,UAAU;AAC9C;AAEA,SAAgB,OAAO,QAAgB;CACrC,OAAQ,KAAK,KAAK,SAAU;AAC9B;AAEA,SAAgB,OAAO,QAAgB;CACrC,OAAQ,MAAM,SAAU,KAAK;AAC/B;AAEA,SAAgB,SACd,OACA,OACA,OACA,QACA,QACA;CACA,QAAS,QAAQ,UAAU,QAAQ,UAAW,SAAS,UAAU;AACnE;AAEA,SAAgB,SAAS,IAAY;CACnC,OAAO,KAAK,IAAI,IAAI,KAAK,EAAE;AAC7B;AAEA,SAAgB,SAAS,MAAc;CACrC,OAAO,MAAM,KAAK,IAAI,IAAI,IAAI,KAAK;AACrC;;;ACzFA,SAAS,YAAY,KAAa,KAAa;CAC7C,IAAI,OAAO,KAAK,MAAM,IAAI,WAAW,yBAAyB;AAChE;AAEA,SAAS,qBAAqB,OAAe,MAAc;CACzD,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,SAAS,GACtC,MAAM,IAAI,WAAW,GAAG,KAAK,0CAA0C;AAE3E;;;;;;;AAQA,MAAa,cAAqB;CAChC,YAAY,OAAO,KAAK,QAAQ,eAAe,OAAO,KAAK,GAAG;CAC9D,cAAc,UAAU,KAAK,QAAQ,SAAS,UAAU,KAAK,GAAG;AAClE;;;;;;;;;;;;;;;;;AAkBA,SAAgB,UAAU,MAAqB;CAC7C,qBAAqB,MAAM,WAAW;CACtC,OAAO;EAGL,YAAY,OAAO,KAAK,QACtB,KAAK,IAAI,eAAe,OAAO,KAAK,GAAG,GAAG,IAAI;EAChD,cAAc,UAAU,KAAK,QAC3B,SACE,SAAS,IACL,WACA,KAAK,IAAI,KAAK,IAAI,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,IAAI,GACnD,KACA,GACF;CACJ;AACF;;;;;AAMA,SAAgB,oBACd,aACA,KACA,KACA;CACA,YAAY,KAAK,GAAG;CACpB,IAAI,EAAE,MAAM,eAAe,cAAc,MACvC,MAAM,IAAI,WAAW,uCAAuC;CAC9D,OAAO,KAAK,IAAI,EAAG,IAAI,KAAK,KAAK,cAAc,QAAQ,MAAM,IAAI;AACnE;;;;;;;;;;;;AAaA,MAAa,mBAA0B;CACrC,YAAY,OAAO,KAAK,QAAQ;EAC9B,uBAAuB,KAAK,GAAG;EAI/B,MAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,GAAG,CAAC;EACpC,MAAM,MAAM,KAAK,IAAI,KAAK,IAAI,GAAG,CAAC;EAClC,OAAO,OACJ,KAAK,IAAI,KAAK,IAAI,MAAM,OAAO,KAAK,GAAG,CAAC,CAAC,IAAI,UAAU,MAAM,QAC9D,GACA,CACF;CACF;CACA,cAAc,UAAU,KAAK,QAAQ;EACnC,uBAAuB,KAAK,GAAG;EAC/B,MAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,GAAG,CAAC;EAEpC,MAAM,YAAY,KAAK,IAAI,SADf,KAAK,IAAI,KAAK,IAAI,GAAG,CACK,IAAI,SAAS,MAAM,UAAU,GAAG,CAAC,CAAC;EACxE,OAAO,KAAK,KAAK,GAAG,IAAI;CAC1B;AACF;AAEA,SAAS,uBAAuB,KAAa,KAAa;CACxD,YAAY,KAAK,GAAG;CACpB,IAAI,QAAQ,KAAK,QAAQ,KAAK,KAAK,KAAK,GAAG,MAAM,KAAK,KAAK,GAAG,GAC5D,MAAM,IAAI,WACR,iFACF;AAEJ;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,WAAW,OAAsB;CAG/C,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,KAAK,IAAI,KAAK,IAAI,IAC/C,MAAM,IAAI,WACR,uDACF;CAIF,IAAI,KAAK,IAAI,KAAK,IAAI,MAAO,OAAO;CAEpC,OAAO;EACL,YAAY,OAAO,KAAK,QAAQ;GAC9B,YAAY,KAAK,GAAG;GACpB,MAAM,aAAa,OAAO,QAAQ,QAAQ,MAAM,MAAM,GAAG,CAAC;GAC1D,IAAI,eAAe,KAAK,eAAe,GAAG,OAAO;GACjD,IAAI,QAAQ,GACV,OACE,IAAI,KAAK,IAAI,cAAc,IAAI,cAAc,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI;GAGrE,OAAO,KAAK,MAAM,aAAa,KAAK,MAAM,KAAK,CAAC,IAAI;EACtD;EACA,cAAc,UAAU,KAAK,QAAQ;GACnC,YAAY,KAAK,GAAG;GACpB,MAAM,IAAI,MAAM,UAAU,GAAG,CAAC;GAC9B,IAAI,MAAM,GAAG,OAAO;GACpB,IAAI,MAAM,GAAG,OAAO;GACpB,MAAM,aACJ,QAAQ,IACH,KAAK,IAAI,SAAS,IAAI,EAAE,KAAK,IAAI,KAAK,IAAI,CAAC,QAAQ,CAAC,MACpD,IAAI,KAAK,IAAI,CAAC,KAAK,KACpB,KAAK,MAAM,QAAQ,CAAC,IAAI,KAAK,MAAM,KAAK;GAC9C,OAAO,OAAO,MAAM,OAAO;EAC7B;CACF;AACF;;;;;;;;;;;;;AAcA,SAAgB,mBAAmB,MAAqB;CACtD,qBAAqB,MAAM,oBAAoB;CAC/C,OAAO;EACL,YAAY,OAAO,KAAK,QAAQ;GAC9B,YAAY,KAAK,GAAG;GACpB,MAAM,aAAa,OAAO,QAAQ,QAAQ,MAAM,MAAM,GAAG,CAAC;GAC1D,IAAI,SAAS,GAAG,OAAO;GACvB,MAAM,qBAAqB,IAAI,aAAa;GAC5C,QACG,IACC,KAAK,IAAI,KAAK,IAAI,kBAAkB,GAAG,IAAI,IACzC,KAAK,KAAK,kBAAkB,KAChC;EAEJ;EACA,cAAc,UAAU,KAAK,QAAQ;GACnC,YAAY,KAAK,GAAG;GAEpB,IAAI,qBAAqB,IADf,MAAM,UAAU,GAAG,CACA,IAAI;GACjC,IAAI,SAAS,KAAK,uBAAuB,GACvC,qBACE,KAAK,IAAI,KAAK,IAAI,kBAAkB,GAAG,IAAI,IAAI,IAC/C,KAAK,KAAK,kBAAkB;GAEhC,OAAO,OAAQ,MAAM,OAAO,KAAM,IAAI;EACxC;CACF;AACF;;;;;AAMA,SAAgB,qBACd,aACA,KACA,KACA;CACA,YAAY,KAAK,GAAG;CACpB,IAAI,EAAE,MAAM,eAAe,cAAc,MACvC,MAAM,IAAI,WAAW,uCAAuC;CAG9D,MAAM,cAAc,cAAc,QAAQ,MAAM;CAChD,OAAO,IAAI,KAAK,IAAI,IAAI,aAAa,CAAC;AACxC;;;;;;;;;;ACvPA,SAAS,IAAI,GAAW,GAAW;CACjC,QAAS,IAAI,IAAK,KAAK;AACzB;AAEA,MAAa,YAAY;CAAC;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;AAAG;AAI3D,MAAa,WAAW;CACtB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAIA,SAAS,kBAAkB,OAAe,MAAc;CACtD,IAAI,CAAC,OAAO,cAAc,KAAK,GAC7B,MAAM,IAAI,WAAW,GAAG,KAAK,+BAA+B;AAEhE;AAEA,SAAgB,cAAc,UAAkB;CAC9C,MAAM,IAAI,SAAS,MAAM,kCAAkC;CAC3D,IAAI,CAAC,GAAG,MAAM,IAAI,MAAM,mBAAmB;CAC3C,MAAM,GAAG,QAAQ,YAAY,UAAU;CACvC,MAAM,eAAe,OAAO,MAAM;CAClC,kBAAkB,cAAc,QAAQ;CACxC,OAAO;EACL,QAAQ,OAAO,kBAAkB;EACrB;EACZ,QAAQ;CACV;AACF;;;;AAKA,SAAgB,WAAW,UAAkB;CAC3C,MAAM,EAAE,QAAQ,YAAY,WAAW,cAAc,QAAQ;CAC7D,MAAM,YAAY,SAAS,QAAQ,OAAO,kBAAkB,CAAY;CACxE,MAAM,mBAAmB,WAAW,OAAO,MAAM,KAAK,KAAK,WAAW;CACtE,MAAM,SAAS,YAAY,MAAM,SAAS,KAAK;CAC/C,kBAAkB,QAAQ,aAAa;CACvC,OAAO;AACT;;;;;;;AAQA,SAAgB,SAAS,YAA2C;CAClE,kBAAkB,YAAY,aAAa;CAC3C,MAAM,YAAY,IAAI,YAAY,EAAE;CACpC,MAAM,SAAS,KAAK,MAAM,aAAa,EAAE,IAAI;CAC7C,OAAO,GAAG,SAAS,aAAa;AAClC;;;;AAKA,SAAgB,QAAQ,YAA6B;CACnD,kBAAkB,YAAY,aAAa;CAC3C,OAAO,SAAS,IAAI,YAAY,EAAE;AACpC;;;;AAKA,SAAgB,WAAW,MAAuB;CAChD,MAAM,IAAI,OAAO,SAAS,WAAW,WAAW,IAAI,IAAI;CACxD,kBAAkB,GAAG,aAAa;CAClC,OACE,IAAI,GAAG,EAAE,MAAM,KACf,IAAI,GAAG,EAAE,MAAM,KACf,IAAI,GAAG,EAAE,MAAM,KACf,IAAI,GAAG,EAAE,MAAM,KACf,IAAI,GAAG,EAAE,MAAM,KACf,IAAI,GAAG,EAAE,MAAM,KACf,IAAI,GAAG,EAAE,MAAM;AAEnB;;;;AAKA,SAAgB,WAAW,MAAuB;CAChD,OAAO,CAAC,WAAW,IAAI;AACzB;;;;;;;AAQA,SAAgB,gBAAgB,MAAuB,SAAS,GAAG,KAAK,KAAK;CAC3E,MAAM,IAAI,OAAO,SAAS,WAAW,WAAW,IAAI,IAAI;CACxD,kBAAkB,GAAG,aAAa;CAClC,OAAQ,KAAK,KAAM,OAAO,IAAI,IAAI,SAAS,OAAO;AACpD;;;;;;;AAQA,MAAa,oBAAoB;;;;;;;;;;AAWjC,SAAgB,mBAAmB,OAAe;CAChD,MAAM,SAAS,QAAQ;CACvB,OAAO,UAAU,SAAS,IAAI,oBAAoB;AACpD;;;;;;;;;;;;AAaA,MAAa,iBAAiB;CAC5B,OAAO;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAAE;CAC5B,cAAc;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAAE;CACnC,eAAe;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAAE;CACpC,cAAc;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAAE;CAEnC,QAAQ;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAAE;CAC7B,QAAQ;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAAE;CAC7B,UAAU;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAAE;CAC/B,QAAQ;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAAE;CAC7B,YAAY;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAAE;CACjC,SAAS;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAAE;CAC9B,SAAS;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAAE;CAE9B,iBAAiB;EAAC;EAAG;EAAG;EAAG;EAAG;CAAC;CAC/B,iBAAiB;EAAC;EAAG;EAAG;EAAG;EAAG;CAAE;CAChC,OAAO;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;CAAE;CAEzB,WAAW;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;CAAE;CAC7B,WAAW;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAI;CAAE;AAClD;;;;;;;;;;;;AAeA,SAAgB,QACd,MACA,MACA,MACS;CACT,MAAM,IAAI,OAAO,SAAS,WAAW,WAAW,IAAI,IAAI;CACxD,MAAM,IAAI,OAAO,SAAS,WAAW,WAAW,IAAI,IAAI;CACxD,kBAAkB,GAAG,aAAa;CAClC,kBAAkB,GAAG,kBAAkB;CACvC,OAAQ,eAAe,KAAK,CAAuB,SAAS,IAAI,IAAI,GAAG,EAAE,CAAC;AAC5E;;;;;;;;;;;;;;;AAgBA,SAAgB,WACd,MACA,MACA,UAAU,GACA;CACV,MAAM,IAAI,OAAO,SAAS,WAAW,WAAW,IAAI,IAAI;CACxD,kBAAkB,GAAG,kBAAkB;CACvC,IAAI,CAAC,OAAO,cAAc,OAAO,KAAK,UAAU,GAC9C,MAAM,IAAI,WAAW,oDAAoD;CAE3E,MAAM,YAAY,eAAe;CACjC,OAAO,MAAM,KAAK,EAAE,QAAQ,QAAQ,IAAI,GAAG,WACzC,UAAU,KAAK,aAAa,IAAI,SAAS,KAAK,QAAQ,CACxD,CAAC,CAAC,KAAK;AACT;;;;ACxNA,MAAM,WAA0C;CAC9C,CAAC,KAAK,KAAK;CACX,CAAC,KAAK,IAAI;CACV,CAAC,KAAK,IAAI;CACV,CAAC,KAAK,IAAI;CACV,CAAC,IAAI,CAAC;CACN,CAAC,KAAK,GAAG;CACT,CAAC,KAAK,GAAG;CACT,CAAC,KAAK,GAAG;AACX;AAEA,MAAM,eAAe,IAAI,IAAoB,QAAQ;;;;;;;AAQrD,MAAM,gBAA0C;CAAE,GAAG;CAAK,GAAG;AAAI;;;;;;;AA6CjE,SAAS,QAAQ,OAAe,OAAuB;CACrD,OAAO,YAAY,QAAQ,KAAK;AAClC;;AAGA,MAAM,mBACJ;;;;;;;;;;;;;;;;;AAkBF,SAAgB,WACd,MACA,UAA6B,CAAC,GACf;CACf,MAAM,EAAE,OAAO,IAAI,WAAW,MAAM,QAAQ,YAAY,OAAO;CAC/D,IAAI,SAAS,MAAM,SAAS,IAC1B,MAAM,IAAI,WAAW,4CAA4C;CAEnE,MAAM,YAAY,aAAa,IAAI,IAAI,KAAK;;;;;CAM5C,MAAM,SAAS,UAAkB;EAC/B,MAAM,OAAO,WAAW,KAAA,IAAY,MAAM,QAAQ,MAAM,IAAI,OAAO,KAAK;EACxE,OAAO,OAAO,IAAI,MAAM,IAAI,KAAK,QAAQ,KAAK,EAAE,IAAI;CACtD;CAEA,IAAI,CAAC,UAAU;EAGb,MAAM,SAAS,OAAO;EACtB,OAAO;GACL,SAAS,UACP,OAAO,SAAS,KAAK,IACjB,MAAM,KAAK,IAAI,YAAY,SAC3B,OAAO,KAAK;GAGlB,QAAQ,SAAS;IACf,MAAM,QAAQ,KAAK,KAAK,CAAC,CAAC,MAAM,gBAAgB;IAChD,IAAI,CAAC,OAAO,OAAO;IACnB,MAAM,QAAQ,OAAO,MAAM,EAAE;IAC7B,OAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;GAC1C;EACF;CACF;CAEA,OAAO;EACL,SAAS,UAAU;GACjB,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG,OAAO,OAAO,KAAK;GAChD,MAAM,KAAK,QAAQ;GAEnB,IAAI,QAAQ,SAAS,WAAW,GAAG,WAAW,UAAU,CAAC;GACzD,IAAI,OAAO,GAAG;IAIZ,MAAM,YAAY,KAAK,IAAI,EAAE;IAC7B,QAAQ;IACR,KAAK,IAAI,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,KACxC,IAAI,aAAa,SAAS,EAAE,CAAC,IAAI;KAC/B,QAAQ;KACR;IACF;GAEJ;GACA,IAAI,OAAO,MAAM,QAAQ,IAAI,SAAS,MAAM,CAAC,EAAE,CAAC;GAIhD,IAAI,KAAK,IAAI,OAAO,IAAI,CAAC,KAAK,OAAQ,QAAQ,SAAS,SAAS,GAAG;IACjE,SAAS;IACT,OAAO,MAAM,QAAQ,IAAI,SAAS,MAAM,CAAC,EAAE,CAAC;GAC9C;GACA,OAAO,OAAO,YAAY,SAAS,MAAM,CAAC,KAAK;EACjD;EAEA,QAAQ,SAAS;GACf,MAAM,QAAQ,KAAK,KAAK,CAAC,CAAC,MAAM,gBAAgB;GAChD,IAAI,CAAC,OAAO,OAAO;GACnB,MAAM,SAAS,OAAO,MAAM,EAAE;GAC9B,IAAI,CAAC,OAAO,SAAS,MAAM,GAAG,OAAO;GAErC,IAAI,SAAS,MAAM,EAAE,CAAC,KAAK;GAC3B,MAAM,gBAAgB,UAAU,KAAK;GACrC,IAAI,kBAAkB,MAAM,OAAO,WAAW,aAAa,GACzD,SAAS,OAAO,MAAM,cAAc,MAAM,CAAC,CAAC,KAAK;GAInD,IAAI,WAAW,IAAI,OAAO;GAK1B,IAAI,SAAwB;GAC5B,IAAI,SAAS,MAAM,OAAO,SAAS,IAAI,GACrC,SAAS,OAAO,MAAM,GAAG,OAAO,SAAS,KAAK,MAAM;QAC/C,IAAI,OAAO,UAAU,GAC1B,SAAS;GAEX,IAAI,WAAW,MAAM,OAAO;GAE5B,MAAM,aAAa,cAAc,WAAW;GAC5C,MAAM,QAAQ,aAAa,IAAI,UAAU;GAGzC,IAAI,UAAU,KAAA,GAAW,OAAO;GAChC,OAAQ,SAAS,QAAS;EAC5B;CACF;AACF"}
package/dist/index.d.cts CHANGED
@@ -220,6 +220,23 @@ export declare function isBlackKey(note: number | string): boolean;
220
220
  * @returns frequency [Hz]
221
221
  */
222
222
  export declare function noteToFrequency(note: number | string, detune?: number, a4?: number): number;
223
+ /**
224
+ * Centre of the 14-bit pitch bend range: no bend.
225
+ *
226
+ * The range is not symmetric — 0 is 8192 below centre and 16383 is 8191 above
227
+ * — so a wheel at rest reports exactly this rather than half of the maximum.
228
+ */
229
+ export declare const PITCH_BEND_CENTER = 8192;
230
+ /**
231
+ * A 14-bit pitch bend (0-16383) as -1 to 1, with 0 at rest.
232
+ *
233
+ * Each side of the centre is divided by its own length, since there is one
234
+ * more step below it than above: 0 gives -1 and 16383 gives 1 exactly.
235
+ * Dividing both sides by 8192 would never quite reach 1.
236
+ *
237
+ * @param value pitch bend: 0 ~ 16383
238
+ */
239
+ export declare function normalizePitchBend(value: number): number;
223
240
  /**
224
241
  * Semitones above the root, for each supported scale.
225
242
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/math.ts","../src/scales.ts","../src/midi.ts","../src/unit.ts"],"mappings":";;;;wBAGgB,MAAM,eAAe,aAAa;;;;;;;wBAUlC,eAAe,eAAe,aAAa;;;;;;wBAU3C,SAAS,yBAAyB,aAAa;;;;;;;;;;;wBAe/C,UAAU,eAAe;wBAUzB,QAAQ,WAAW;;;;;;;;;;;;;;;;;;;;;wBA+BnB,YAAY,WAAW;wBAUvB,OAAO;wBAIP,OAAO;wBAIP,SACd,eACA,eACA,eACA,gBACA;wBAKc,SAAS;wBAIT,SAAS;;;;;;;;;;;;;;;;;;UC9FR;;EAEf,YAAY,eAAe,aAAa;;EAExC,cAAc,kBAAkB,aAAa;;;;;;;;qBAmBlC,aAAa;;;;;;;;;;;;;;;;;wBAqBV,UAAU,eAAe;;;;;wBAsBzB,oBACd,qBACA,aACA;;;;;;;;;;;;qBAmBW,kBAAkB;;;;;;;;;;;;;;;;;;;;;;wBAqDf,WAAW,gBAAgB;;;;;;;;;;;;;wBAmD3B,mBAAmB,eAAe;;;;;wBAiClC,qBACd,qBACA,aACA;;;;;;;;UAkBe;EACf;EACA;;;;EAIA;;;;;;EAMA,QAAQ;;;;qBCxQG;KAED,mBAAmB;qBAElB;KAeD,kBAAkB;wBAQd,cAAc;EAOY,QAAA;EACZ;;;;;;wBAQd,WAAW;;;;;;;wBAeX,SAAS,wBAAwB;;;;wBAUjC,QAAQ,qBAAqB;;;;wBAQ7B,WAAW;;;;wBAiBX,WAAW;;;;;;;wBAUX,gBAAgB,uBAAuB,iBAAY;;;;;;;;;;;;qBAiBtD;;;;;;;;;;;;;;;;;;KAsBD,yBAAyB;;;;;;;;;;;;wBAarB,QACd,uBACA,uBACA,MAAM;;;;;;;;;;;;;;;wBAuBQ,WACd,uBACA,MAAM,WACN;;;;;;;;;;KC1LU;UAwBK;;;;;;;;;EASf,OAAO;;;;;;;;;;EAUP;;;;EAIA;;;;;EAKA;;;UAIe;EACf,SAAS;EACT,QAAQ;;;;;;;;;;;;;;;;;;wBAiCM,WACd,cACA,UAAS,oBACR"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/math.ts","../src/scales.ts","../src/midi.ts","../src/unit.ts"],"mappings":";;;;wBAGgB,MAAM,eAAe,aAAa;;;;;;;wBAUlC,eAAe,eAAe,aAAa;;;;;;wBAU3C,SAAS,yBAAyB,aAAa;;;;;;;;;;;wBAe/C,UAAU,eAAe;wBAUzB,QAAQ,WAAW;;;;;;;;;;;;;;;;;;;;;wBA+BnB,YAAY,WAAW;wBAUvB,OAAO;wBAIP,OAAO;wBAIP,SACd,eACA,eACA,eACA,gBACA;wBAKc,SAAS;wBAIT,SAAS;;;;;;;;;;;;;;;;;;UC9FR;;EAEf,YAAY,eAAe,aAAa;;EAExC,cAAc,kBAAkB,aAAa;;;;;;;;qBAmBlC,aAAa;;;;;;;;;;;;;;;;;wBAqBV,UAAU,eAAe;;;;;wBAsBzB,oBACd,qBACA,aACA;;;;;;;;;;;;qBAmBW,kBAAkB;;;;;;;;;;;;;;;;;;;;;;wBAqDf,WAAW,gBAAgB;;;;;;;;;;;;;wBAmD3B,mBAAmB,eAAe;;;;;wBAiClC,qBACd,qBACA,aACA;;;;;;;;UAkBe;EACf;EACA;;;;EAIA;;;;;;EAMA,QAAQ;;;;qBCxQG;KAED,mBAAmB;qBAElB;KAeD,kBAAkB;wBAQd,cAAc;EAOY,QAAA;EACZ;;;;;;wBAQd,WAAW;;;;;;;wBAeX,SAAS,wBAAwB;;;;wBAUjC,QAAQ,qBAAqB;;;;wBAQ7B,WAAW;;;;wBAiBX,WAAW;;;;;;;wBAUX,gBAAgB,uBAAuB,iBAAY;;;;;;;qBAYtD;;;;;;;;;;wBAWG,mBAAmB;;;;;;;;;;;;qBAgBtB;;;;;;;;;;;;;;;;;;KAsBD,yBAAyB;;;;;;;;;;;;wBAarB,QACd,uBACA,uBACA,MAAM;;;;;;;;;;;;;;;wBAuBQ,WACd,uBACA,MAAM,WACN;;;;;;;;;;KChNU;UAwBK;;;;;;;;;EASf,OAAO;;;;;;;;;;EAUP;;;;EAIA;;;;;EAKA;;;UAIe;EACf,SAAS;EACT,QAAQ;;;;;;;;;;;;;;;;;;wBAiCM,WACd,cACA,UAAS,oBACR"}
package/dist/index.d.ts CHANGED
@@ -220,6 +220,23 @@ export declare function isBlackKey(note: number | string): boolean;
220
220
  * @returns frequency [Hz]
221
221
  */
222
222
  export declare function noteToFrequency(note: number | string, detune?: number, a4?: number): number;
223
+ /**
224
+ * Centre of the 14-bit pitch bend range: no bend.
225
+ *
226
+ * The range is not symmetric — 0 is 8192 below centre and 16383 is 8191 above
227
+ * — so a wheel at rest reports exactly this rather than half of the maximum.
228
+ */
229
+ export declare const PITCH_BEND_CENTER = 8192;
230
+ /**
231
+ * A 14-bit pitch bend (0-16383) as -1 to 1, with 0 at rest.
232
+ *
233
+ * Each side of the centre is divided by its own length, since there is one
234
+ * more step below it than above: 0 gives -1 and 16383 gives 1 exactly.
235
+ * Dividing both sides by 8192 would never quite reach 1.
236
+ *
237
+ * @param value pitch bend: 0 ~ 16383
238
+ */
239
+ export declare function normalizePitchBend(value: number): number;
223
240
  /**
224
241
  * Semitones above the root, for each supported scale.
225
242
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/math.ts","../src/scales.ts","../src/midi.ts","../src/unit.ts"],"mappings":";;;;wBAGgB,MAAM,eAAe,aAAa;;;;;;;wBAUlC,eAAe,eAAe,aAAa;;;;;;wBAU3C,SAAS,yBAAyB,aAAa;;;;;;;;;;;wBAe/C,UAAU,eAAe;wBAUzB,QAAQ,WAAW;;;;;;;;;;;;;;;;;;;;;wBA+BnB,YAAY,WAAW;wBAUvB,OAAO;wBAIP,OAAO;wBAIP,SACd,eACA,eACA,eACA,gBACA;wBAKc,SAAS;wBAIT,SAAS;;;;;;;;;;;;;;;;;;UC9FR;;EAEf,YAAY,eAAe,aAAa;;EAExC,cAAc,kBAAkB,aAAa;;;;;;;;qBAmBlC,aAAa;;;;;;;;;;;;;;;;;wBAqBV,UAAU,eAAe;;;;;wBAsBzB,oBACd,qBACA,aACA;;;;;;;;;;;;qBAmBW,kBAAkB;;;;;;;;;;;;;;;;;;;;;;wBAqDf,WAAW,gBAAgB;;;;;;;;;;;;;wBAmD3B,mBAAmB,eAAe;;;;;wBAiClC,qBACd,qBACA,aACA;;;;;;;;UAkBe;EACf;EACA;;;;EAIA;;;;;;EAMA,QAAQ;;;;qBCxQG;KAED,mBAAmB;qBAElB;KAeD,kBAAkB;wBAQd,cAAc;EAOY,QAAA;EACZ;;;;;;wBAQd,WAAW;;;;;;;wBAeX,SAAS,wBAAwB;;;;wBAUjC,QAAQ,qBAAqB;;;;wBAQ7B,WAAW;;;;wBAiBX,WAAW;;;;;;;wBAUX,gBAAgB,uBAAuB,iBAAY;;;;;;;;;;;;qBAiBtD;;;;;;;;;;;;;;;;;;KAsBD,yBAAyB;;;;;;;;;;;;wBAarB,QACd,uBACA,uBACA,MAAM;;;;;;;;;;;;;;;wBAuBQ,WACd,uBACA,MAAM,WACN;;;;;;;;;;KC1LU;UAwBK;;;;;;;;;EASf,OAAO;;;;;;;;;;EAUP;;;;EAIA;;;;;EAKA;;;UAIe;EACf,SAAS;EACT,QAAQ;;;;;;;;;;;;;;;;;;wBAiCM,WACd,cACA,UAAS,oBACR"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/math.ts","../src/scales.ts","../src/midi.ts","../src/unit.ts"],"mappings":";;;;wBAGgB,MAAM,eAAe,aAAa;;;;;;;wBAUlC,eAAe,eAAe,aAAa;;;;;;wBAU3C,SAAS,yBAAyB,aAAa;;;;;;;;;;;wBAe/C,UAAU,eAAe;wBAUzB,QAAQ,WAAW;;;;;;;;;;;;;;;;;;;;;wBA+BnB,YAAY,WAAW;wBAUvB,OAAO;wBAIP,OAAO;wBAIP,SACd,eACA,eACA,eACA,gBACA;wBAKc,SAAS;wBAIT,SAAS;;;;;;;;;;;;;;;;;;UC9FR;;EAEf,YAAY,eAAe,aAAa;;EAExC,cAAc,kBAAkB,aAAa;;;;;;;;qBAmBlC,aAAa;;;;;;;;;;;;;;;;;wBAqBV,UAAU,eAAe;;;;;wBAsBzB,oBACd,qBACA,aACA;;;;;;;;;;;;qBAmBW,kBAAkB;;;;;;;;;;;;;;;;;;;;;;wBAqDf,WAAW,gBAAgB;;;;;;;;;;;;;wBAmD3B,mBAAmB,eAAe;;;;;wBAiClC,qBACd,qBACA,aACA;;;;;;;;UAkBe;EACf;EACA;;;;EAIA;;;;;;EAMA,QAAQ;;;;qBCxQG;KAED,mBAAmB;qBAElB;KAeD,kBAAkB;wBAQd,cAAc;EAOY,QAAA;EACZ;;;;;;wBAQd,WAAW;;;;;;;wBAeX,SAAS,wBAAwB;;;;wBAUjC,QAAQ,qBAAqB;;;;wBAQ7B,WAAW;;;;wBAiBX,WAAW;;;;;;;wBAUX,gBAAgB,uBAAuB,iBAAY;;;;;;;qBAYtD;;;;;;;;;;wBAWG,mBAAmB;;;;;;;;;;;;qBAgBtB;;;;;;;;;;;;;;;;;;KAsBD,yBAAyB;;;;;;;;;;;;wBAarB,QACd,uBACA,uBACA,MAAM;;;;;;;;;;;;;;;wBAuBQ,WACd,uBACA,MAAM,WACN;;;;;;;;;;KChNU;UAwBK;;;;;;;;;EASf,OAAO;;;;;;;;;;EAUP;;;;EAIA;;;;;EAKA;;;UAIe;EACf,SAAS;EACT,QAAQ;;;;;;;;;;;;;;;;;;wBAiCM,WACd,cACA,UAAS,oBACR"}
package/dist/index.js CHANGED
@@ -355,6 +355,26 @@ function noteToFrequency(note, detune = 0, a4 = 440) {
355
355
  return a4 / 32 * 2 ** ((n - 9 + detune / 100) / 12);
356
356
  }
357
357
  /**
358
+ * Centre of the 14-bit pitch bend range: no bend.
359
+ *
360
+ * The range is not symmetric — 0 is 8192 below centre and 16383 is 8191 above
361
+ * — so a wheel at rest reports exactly this rather than half of the maximum.
362
+ */
363
+ const PITCH_BEND_CENTER = 8192;
364
+ /**
365
+ * A 14-bit pitch bend (0-16383) as -1 to 1, with 0 at rest.
366
+ *
367
+ * Each side of the centre is divided by its own length, since there is one
368
+ * more step below it than above: 0 gives -1 and 16383 gives 1 exactly.
369
+ * Dividing both sides by 8192 would never quite reach 1.
370
+ *
371
+ * @param value pitch bend: 0 ~ 16383
372
+ */
373
+ function normalizePitchBend(value) {
374
+ const offset = value - PITCH_BEND_CENTER;
375
+ return offset / (offset < 0 ? PITCH_BEND_CENTER : 8191);
376
+ }
377
+ /**
358
378
  * Semitones above the root, for each supported scale.
359
379
  *
360
380
  * Every entry starts at 0 and stays inside one octave, so a scale is a set of
@@ -665,6 +685,6 @@ function unitFormat(unit, options = {}) {
665
685
  };
666
686
  }
667
687
  //#endregion
668
- export { clamp, curveScale, curveWithCenterValue, dbToGain, degree, exponentialScale, gainToDb, inScale, isBlackKey, isWhiteKey, linearScale, mapValue, normalizeValue, noteKey, noteKeys, noteName, noteNumber, noteToFrequency, parseNoteName, radian, rawValue, scaleIntervals, scaleNotes, skewScale, skewWithCenterValue, stepValue, symmetricSkewScale, toFixed, toPrecision, unitFormat, whiteKeys };
688
+ export { PITCH_BEND_CENTER, clamp, curveScale, curveWithCenterValue, dbToGain, degree, exponentialScale, gainToDb, inScale, isBlackKey, isWhiteKey, linearScale, mapValue, normalizePitchBend, normalizeValue, noteKey, noteKeys, noteName, noteNumber, noteToFrequency, parseNoteName, radian, rawValue, scaleIntervals, scaleNotes, skewScale, skewWithCenterValue, stepValue, symmetricSkewScale, toFixed, toPrecision, unitFormat, whiteKeys };
669
689
 
670
690
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/math.ts","../src/scales.ts","../src/midi.ts","../src/unit.ts"],"sourcesContent":["/**\n * clamp value between min and max\n */\nexport function clamp(value: number, min: number, max: number) {\n return Math.max(min, Math.min(value, max))\n}\n\n/**\n * Normalize the value from 0 to 1, spreading the range evenly.\n *\n * This is the linear mapping and takes no curve of its own; a `Scale` builds\n * whatever curve it needs on top of it.\n */\nexport function normalizeValue(value: number, min: number, max: number) {\n if (min >= max) throw new RangeError('requirements: min < max')\n return clamp((value - min) / (max - min), 0, 1)\n}\n\n/**\n * Convert normalized values back to raw values, spreading the range evenly.\n *\n * The inverse of {@link normalizeValue}.\n */\nexport function rawValue(normalizedValue: number, min: number, max: number) {\n if (min >= max) throw new RangeError('requirements: min < max')\n return min + clamp(normalizedValue, 0, 1) * (max - min)\n}\n\n/**\n * Put a value on the grid the caller asked for, rounding a half step upwards.\n *\n * The rounding is done on the quotient rather than by comparing the distance\n * to the two neighbours, because both of those carry error of their own. The\n * quotient is cleared of its artefact first: `0.15 / 0.1` is 1.4999999999999998,\n * and a value sitting exactly on a half step would otherwise fall to whichever\n * side the last bit happened to land on — 0.25 rounded up while 0.15 and 0.35\n * rounded down.\n */\nexport function stepValue(value: number, step: number) {\n if (step <= 0) throw new RangeError('requirements: step > 0')\n const quotient = Math.round(toPrecision(value / step))\n // The product has its own debris to drop: 3141593 * 1e-6 is not 3.141593.\n const stepped = toPrecision(quotient * step)\n // Math.round answers -0 for anything in [-0.5, 0), and that reaches the\n // caller's onChange as a value that prints as 0 but is not it.\n return stepped === 0 ? 0 : stepped\n}\n\nexport function toFixed(x: number, fractionDigits?: number) {\n return Number(x.toFixed(fractionDigits))\n}\n\n/**\n * The significant decimal digits a double actually carries. A double holds a\n * little under 16, so anything past this is the binary representation showing\n * through rather than information.\n */\nconst SIGNIFICANT_DIGITS = 15\n\n/**\n * Drop the binary artefact from a computed value.\n *\n * Arithmetic on doubles leaves debris in the last couple of digits, and it\n * accumulates: adding 0.1 to 5 twelve times gives 5.699999999999998 rather\n * than 5.7, and the display of a control shows exactly that. Rounding to the\n * digits a double can carry removes it, and adds nothing back — the value was\n * already the result of a calculation whose own error is that size or larger.\n *\n * This is not the same as rounding to a `step`. {@link stepValue} puts a value\n * on a grid the caller asked for and is a decision about the value; this only\n * removes what was never in the value to begin with.\n *\n * @param significantDigits how many digits to keep. The default is the only\n * one that is purely artefact removal; a smaller number starts discarding real\n * precision.\n *\n * @example\n * toPrecision(5.1 + 0.1) // 5.2, rather than 5.199999999999999\n */\nexport function toPrecision(x: number, significantDigits = SIGNIFICANT_DIGITS) {\n // Zero has no significant digits to round to, and a non-finite value has no\n // decimal form to parse back.\n if (x === 0 || !Number.isFinite(x)) return x\n const rounded = Number(x.toPrecision(significantDigits))\n // Rounding up at the very top of the range overflows to Infinity, which is\n // a worse answer than the artefact.\n return Number.isFinite(rounded) ? rounded : x\n}\n\nexport function radian(degree: number) {\n return (Math.PI * degree) / 180\n}\n\nexport function degree(radian: number) {\n return (180 * radian) / Math.PI\n}\n\nexport function mapValue(\n value: number,\n inMin: number,\n inMax: number,\n outMin: number,\n outMax: number,\n) {\n return ((value - inMin) / (inMax - inMin)) * (outMax - outMin) + outMin\n}\n\nexport function dbToGain(db: number) {\n return Math.pow(10, db / 20)\n}\n\nexport function gainToDb(gain: number) {\n return 20 * (Math.log(gain) / Math.LN10)\n}\n","import { clamp, normalizeValue, rawValue } from './math'\n\n/**\n * How a value is distributed across the travel of a control.\n *\n * `normalize` and `denormalize` are inverses of each other: the position is\n * 0 at `min` and 1 at `max`, and everything in between is up to the scale.\n *\n * `min` and `max` are arguments rather than baked into the scale, so a scale\n * holds no state and can be a module level constant. Passing the same object\n * on every render therefore costs nothing.\n *\n * @example\n * ```ts\n * exponentialScale.denormalize(0.5, 20, 20000) // 632.45…\n * ```\n */\nexport interface Scale {\n /** Value to its position on the travel, 0-1. */\n normalize: (value: number, min: number, max: number) => number\n /** Position on the travel, 0-1, back to a value. */\n denormalize: (position: number, min: number, max: number) => number\n}\n\nfunction assertRange(min: number, max: number) {\n if (min >= max) throw new RangeError('requirements: min < max')\n}\n\nfunction assertPositiveFinite(value: number, name: string) {\n if (!Number.isFinite(value) || value <= 0) {\n throw new RangeError(`${name}: requirements: finite and greater than 0`)\n }\n}\n\n/**\n * Equal travel gives an equal change in value.\n *\n * The right default for anything already linear in perception: dB values,\n * pan, percentages, MIDI note numbers, semitones.\n */\nexport const linearScale: Scale = {\n normalize: (value, min, max) => normalizeValue(value, min, max),\n denormalize: (position, min, max) => rawValue(position, min, max),\n}\n\n/**\n * The power law of JUCE's `NormalisableRange::skew`, applied to `value - min`.\n *\n * Use it when the value has to agree with a JUCE or iPlug2 parameter — a\n * plugin UI in a WebView, say, where the knob must sit exactly where the\n * host's automation curve puts it. {@link skewWithCenterValue} gives the\n * factor that places a chosen value at the middle of the travel.\n *\n * `skew < 1` gives the lower end more travel, `skew > 1` the upper end.\n *\n * For new designs prefer {@link exponentialScale} or {@link curveScale}: the\n * slope of this curve is either zero or infinite at `min`, so the bottom of\n * the range is a dead zone or jumps.\n *\n * @param skew the JUCE skew factor\n */\nexport function skewScale(skew: number): Scale {\n assertPositiveFinite(skew, 'skewScale')\n return {\n // The two expressions JUCE uses, kept verbatim so the numbers agree with\n // a NormalisableRange: pow() one way, exp(log()) the other.\n normalize: (value, min, max) =>\n Math.pow(normalizeValue(value, min, max), skew),\n denormalize: (position, min, max) =>\n rawValue(\n skew === 1\n ? position\n : Math.exp(Math.log(clamp(position, 0, 1)) / skew),\n min,\n max,\n ),\n }\n}\n\n/**\n * The skew factor for {@link skewScale} that puts `centerValue` at the middle\n * of the travel — JUCE's `NormalisableRange::setSkewForCentre`.\n */\nexport function skewWithCenterValue(\n centerValue: number,\n min: number,\n max: number,\n) {\n assertRange(min, max)\n if (!(min < centerValue && centerValue < max))\n throw new RangeError('requirements: min < centerValue < max')\n return Math.log(0.5) / Math.log((centerValue - min) / (max - min))\n}\n\n/**\n * Equal travel gives an equal *ratio*, so an octave — or a percentage — takes\n * the same distance wherever it falls.\n *\n * The scale for frequency (a filter cutoff over 20-20000 Hz), free running\n * rates, and delay times.\n *\n * Requires `min` and `max` to be non-zero and of the same sign, since no\n * ratio reaches zero or crosses it. Use {@link curveScale} for a range that\n * starts at 0.\n */\nexport const exponentialScale: Scale = {\n normalize: (value, min, max) => {\n assertExponentialRange(min, max)\n // The value is clamped before the logarithm, not after: outside the range\n // the ratio can be negative, and log() would give NaN rather than a\n // position to clamp.\n const start = Math.log(Math.abs(min))\n const end = Math.log(Math.abs(max))\n return clamp(\n (Math.log(Math.abs(clamp(value, min, max))) - start) / (end - start),\n 0,\n 1,\n )\n },\n denormalize: (position, min, max) => {\n assertExponentialRange(min, max)\n const start = Math.log(Math.abs(min))\n const end = Math.log(Math.abs(max))\n const magnitude = Math.exp(start + (end - start) * clamp(position, 0, 1))\n return Math.sign(min) * magnitude\n },\n}\n\nfunction assertExponentialRange(min: number, max: number) {\n assertRange(min, max)\n if (min === 0 || max === 0 || Math.sign(min) !== Math.sign(max)) {\n throw new RangeError(\n 'exponentialScale: requirements: min and max are non-zero and have the same sign',\n )\n }\n}\n\n/**\n * An exponential bend that still passes exactly through `min` and `max`, so\n * unlike {@link exponentialScale} it works on a range that starts at 0 or\n * crosses it, and unlike {@link skewScale} its slope is neither zero nor\n * infinite at either end.\n *\n * The general purpose taper, and the same family as the curve of an envelope\n * segment (SuperCollider's `CurveWarp`).\n *\n * - `curve > 0` gives the lower end more travel — envelope times from 0 ms,\n * delay times, anything that wants fine control near the bottom\n * - `curve < 0` gives the upper end more travel — a volume fader over\n * -60..+6 dB that should be precise around 0 dB\n * - near 0 it is indistinguishable from {@link linearScale}, and is treated\n * as linear to avoid dividing by zero\n *\n * {@link curveWithCenterValue} gives the curve that places a chosen value at\n * the middle of the travel.\n *\n * @param curve how hard the curve bends, and in which direction\n */\nexport function curveScale(curve: number): Scale {\n // Beyond this the flatter half of the curve no longer has enough distinct\n // double values for normalize and denormalize to remain inverses.\n if (!Number.isFinite(curve) || Math.abs(curve) > 32) {\n throw new RangeError(\n 'curveScale: requirements: finite curve from -32 to 32',\n )\n }\n // The two coefficients blow up as the curve flattens: `a` divides by\n // 1 - e^curve, which goes to 0.\n if (Math.abs(curve) < 0.001) return linearScale\n\n return {\n normalize: (value, min, max) => {\n assertRange(min, max)\n const proportion = clamp((value - min) / (max - min), 0, 1)\n if (proportion === 0 || proportion === 1) return proportion\n if (curve > 0) {\n return (\n 1 + Math.log(proportion + (1 - proportion) * Math.exp(-curve)) / curve\n )\n }\n return Math.log1p(proportion * Math.expm1(curve)) / curve\n },\n denormalize: (position, min, max) => {\n assertRange(min, max)\n const p = clamp(position, 0, 1)\n if (p === 0) return min\n if (p === 1) return max\n const proportion =\n curve > 0\n ? (Math.exp(curve * (p - 1)) * (1 - Math.exp(-curve * p))) /\n (1 - Math.exp(-curve))\n : Math.expm1(curve * p) / Math.expm1(curve)\n return min + (max - min) * proportion\n },\n }\n}\n\n/**\n * {@link skewScale} mirrored about the middle of the range, so both halves\n * bend the same way — JUCE's `symmetricSkew`.\n *\n * For a bipolar control whose centre matters: detune over -100..+100 cents,\n * or a bipolar filter envelope amount, where the fine adjustment is around 0\n * rather than at either end.\n *\n * `skew < 1` gives the middle more travel, `skew > 1` the two ends.\n *\n * @param skew the JUCE skew factor\n */\nexport function symmetricSkewScale(skew: number): Scale {\n assertPositiveFinite(skew, 'symmetricSkewScale')\n return {\n normalize: (value, min, max) => {\n assertRange(min, max)\n const proportion = clamp((value - min) / (max - min), 0, 1)\n if (skew === 1) return proportion\n const distanceFromMiddle = 2 * proportion - 1\n return (\n (1 +\n Math.pow(Math.abs(distanceFromMiddle), skew) *\n Math.sign(distanceFromMiddle)) /\n 2\n )\n },\n denormalize: (position, min, max) => {\n assertRange(min, max)\n const p = clamp(position, 0, 1)\n let distanceFromMiddle = 2 * p - 1\n if (skew !== 1 && distanceFromMiddle !== 0) {\n distanceFromMiddle =\n Math.pow(Math.abs(distanceFromMiddle), 1 / skew) *\n Math.sign(distanceFromMiddle)\n }\n return min + ((max - min) / 2) * (1 + distanceFromMiddle)\n },\n }\n}\n\n/**\n * The curve for {@link curveScale} that puts `centerValue` at the middle of\n * the travel — the counterpart of {@link skewWithCenterValue}.\n */\nexport function curveWithCenterValue(\n centerValue: number,\n min: number,\n max: number,\n) {\n assertRange(min, max)\n if (!(min < centerValue && centerValue < max)) {\n throw new RangeError('requirements: min < centerValue < max')\n }\n // value(0.5) - min = range / (1 + e^(curve / 2))\n const proportion = (centerValue - min) / (max - min)\n return 2 * Math.log(1 / proportion - 1)\n}\n\n/**\n * How a value is scaled: the range it lives in, how it is rounded, and how it\n * is distributed across the travel.\n *\n * `AxisOptions` of `@tremolo-ui/dom` extends this, so a drag and a\n * wheel / keyboard nudge run the same value pipeline.\n */\nexport interface ValueRange {\n min: number\n max: number\n /**\n * Rounding applied to the value. Left unrounded when omitted.\n */\n step?: number\n /**\n * How the value is distributed across the travel.\n *\n * @default linearScale\n */\n scale?: Scale\n}\n","/**\n * Remainder that follows the sign of the divisor, rather than of the dividend.\n *\n * `%` in JavaScript keeps the sign of the left operand, so `-13 % 12` is `-1`\n * and cannot index a 12-element table. Every note here is placed by its\n * position within the octave, and note numbers below C-1 are negative.\n */\nfunction mod(n: number, m: number) {\n return ((n % m) + m) % m\n}\n\nexport const whiteKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] as const\n\nexport type WhiteKey = (typeof whiteKeys)[number]\n\nexport const noteKeys = [\n 'C',\n 'C#',\n 'D',\n 'D#',\n 'E',\n 'F',\n 'F#',\n 'G',\n 'G#',\n 'A',\n 'A#',\n 'B',\n] as const\n\nexport type NoteKey = (typeof noteKeys)[number]\n\nfunction assertSafeInteger(value: number, name: string) {\n if (!Number.isSafeInteger(value)) {\n throw new RangeError(`${name}: requirements: a safe integer`)\n }\n}\n\nexport function parseNoteName(noteName: string) {\n const m = noteName.match(/^([a-g])(#{0,2}|b{0,2})(-?\\d+)$/i)\n if (!m) throw new Error('Invalid note name')\n const [, letter, accidental, octave] = m\n const parsedOctave = Number(octave)\n assertSafeInteger(parsedOctave, 'octave')\n return {\n letter: letter.toLocaleUpperCase() as WhiteKey,\n accidental: accidental as '#' | '##' | 'b' | 'bb' | '',\n octave: parsedOctave,\n }\n}\n\n/**\n * Convert noteName to noteNumber\n */\nexport function noteNumber(noteName: string) {\n const { letter, accidental, octave } = parseNoteName(noteName)\n const noteIndex = noteKeys.indexOf(letter.toLocaleUpperCase() as NoteKey)\n const accidentalValue = (accidental[0] === 'b' ? -1 : 1) * accidental.length\n const result = noteIndex + 12 * (octave + 1) + accidentalValue\n assertSafeInteger(result, 'note number')\n return result\n}\n\n/**\n * Convert noteNumber to noteName\n * C-1 = 0\n * G9 = 127\n * @param noteNumber noteNumber\n */\nexport function noteName(noteNumber: number): `${NoteKey}${number}` {\n assertSafeInteger(noteNumber, 'note number')\n const noteIndex = mod(noteNumber, 12)\n const octave = Math.floor(noteNumber / 12) - 1\n return `${noteKeys[noteIndex]}${octave}`\n}\n\n/**\n * Convert noteNumber to noteKey\n */\nexport function noteKey(noteNumber: number): NoteKey {\n assertSafeInteger(noteNumber, 'note number')\n return noteKeys[mod(noteNumber, 12)]\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n */\nexport function isWhiteKey(note: number | string) {\n const n = typeof note === 'string' ? noteNumber(note) : note\n assertSafeInteger(n, 'note number')\n return (\n mod(n, 12) === 0 ||\n mod(n, 12) === 2 ||\n mod(n, 12) === 4 ||\n mod(n, 12) === 5 ||\n mod(n, 12) === 7 ||\n mod(n, 12) === 9 ||\n mod(n, 12) === 11\n )\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n */\nexport function isBlackKey(note: number | string) {\n return !isWhiteKey(note)\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n * @param detune [cent]\n * @param a4 A4 frequency [Hz]\n * @returns frequency [Hz]\n */\nexport function noteToFrequency(note: number | string, detune = 0, a4 = 440) {\n const n = typeof note === 'string' ? noteNumber(note) : note\n assertSafeInteger(n, 'note number')\n return (a4 / 32) * 2 ** ((n - 9 + detune / 100) / 12)\n}\n\n/**\n * Semitones above the root, for each supported scale.\n *\n * Every entry starts at 0 and stays inside one octave, so a scale is a set of\n * pitch classes rather than a set of notes: {@link inScale} compares against\n * it with the octave taken out.\n *\n * `ionian` and `aeolian` are the same sets as `major` and `naturalMinor`; both\n * spellings are here because both are what someone reaches for depending on\n * whether they are thinking in keys or in modes.\n */\nexport const scaleIntervals = {\n major: [0, 2, 4, 5, 7, 9, 11],\n naturalMinor: [0, 2, 3, 5, 7, 8, 10],\n harmonicMinor: [0, 2, 3, 5, 7, 8, 11],\n melodicMinor: [0, 2, 3, 5, 7, 9, 11],\n\n ionian: [0, 2, 4, 5, 7, 9, 11],\n dorian: [0, 2, 3, 5, 7, 9, 10],\n phrygian: [0, 1, 3, 5, 7, 8, 10],\n lydian: [0, 2, 4, 6, 7, 9, 11],\n mixolydian: [0, 2, 4, 5, 7, 9, 10],\n aeolian: [0, 2, 3, 5, 7, 8, 10],\n locrian: [0, 1, 3, 5, 6, 8, 10],\n\n majorPentatonic: [0, 2, 4, 7, 9],\n minorPentatonic: [0, 3, 5, 7, 10],\n blues: [0, 3, 5, 6, 7, 10],\n\n wholeTone: [0, 2, 4, 6, 8, 10],\n chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],\n} as const satisfies Record<string, readonly number[]>\n\nexport type ScaleName = keyof typeof scaleIntervals\n\n/**\n * Whether a note belongs to a scale, regardless of the octave either sits in.\n *\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n * @param root the note the scale is built on, in the same two forms\n *\n * @example\n * ```ts\n * inScale('F#4', 'D3', 'major') // true\n * ```\n */\nexport function inScale(\n note: number | string,\n root: number | string,\n name: ScaleName,\n): boolean {\n const n = typeof note === 'string' ? noteNumber(note) : note\n const r = typeof root === 'string' ? noteNumber(root) : root\n assertSafeInteger(n, 'note number')\n assertSafeInteger(r, 'root note number')\n return (scaleIntervals[name] as readonly number[]).includes(mod(n - r, 12))\n}\n\n/**\n * The notes of a scale, ascending from `root`.\n *\n * The octave above the root is not included: ask for more `octaves` instead, so\n * that concatenating the result of two calls does not repeat a note.\n *\n * @param root noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n * @param octaves how many octaves to cover\n *\n * @example\n * ```ts\n * scaleNotes('C3', 'majorPentatonic') // [48, 50, 52, 55, 57]\n * ```\n */\nexport function scaleNotes(\n root: number | string,\n name: ScaleName,\n octaves = 1,\n): number[] {\n const r = typeof root === 'string' ? noteNumber(root) : root\n assertSafeInteger(r, 'root note number')\n if (!Number.isSafeInteger(octaves) || octaves < 0) {\n throw new RangeError('octaves: requirements: a non-negative safe integer')\n }\n const intervals = scaleIntervals[name] as readonly number[]\n return Array.from({ length: octaves }, (_, octave) =>\n intervals.map((interval) => r + octave * 12 + interval),\n ).flat()\n}\n","import { toPrecision } from './math'\n\n/**\n * The SI prefixes {@link unitFormat} chooses between.\n *\n * Deliberately narrower than the full SI set: yocto through yotta are of no\n * use to an audio control, and every extra prefix is one more symbol `parse`\n * has to tell apart from a unit.\n */\nexport type SIPrefix = 'p' | 'n' | 'µ' | 'm' | '' | 'k' | 'M' | 'G'\n\n/** Ordered small to large. The empty symbol is the base unit. */\nconst PREFIXES: readonly [SIPrefix, number][] = [\n ['p', 1e-12],\n ['n', 1e-9],\n ['µ', 1e-6],\n ['m', 1e-3],\n ['', 1],\n ['k', 1e3],\n ['M', 1e6],\n ['G', 1e9],\n]\n\nconst PREFIX_SCALE = new Map<string, number>(PREFIXES)\n\n/**\n * Micro is written three ways. `µ` (U+00B5 MICRO SIGN) is what `format`\n * writes and what d3-format uses, `μ` (U+03BC GREEK SMALL LETTER MU) looks\n * identical and is what a Greek keyboard produces, and `u` is what everyone\n * actually types. All three read back the same.\n */\nconst MICRO_ALIASES: Record<string, SIPrefix> = { μ: 'µ', u: 'µ' }\n\nexport interface UnitFormatOptions {\n /**\n * The prefix the stored value is already in.\n *\n * A control that keeps milliseconds in `value` is `{ base: 'm' }` with a\n * unit of `'s'`: 1500 then displays as `1.5s`, and `parse` gives 1500 back.\n *\n * @default ''\n */\n base?: SIPrefix\n /**\n * Whether to scale the number and pick a prefix at all.\n *\n * Turn it off for anything that is not an SI quantity. dB, %, cents and\n * semitones do not take prefixes, and `-6dB` read as \"-6 deci-B\" is wrong\n * rather than merely unusual.\n *\n * @default true\n */\n prefixes?: boolean\n /**\n * Digits after the decimal point. The number is left as-is when omitted.\n */\n digits?: number\n /**\n * Text placed between the number and the unit.\n * @default ''\n */\n separator?: string\n}\n\n/** The `format` / `parse` pair a `NumberInput` takes. */\nexport interface UnitFormatter {\n format: (value: number) => string\n parse: (text: string) => number\n}\n\n/**\n * Divide by a prefix scale without showing the result of doing so in binary.\n *\n * `0.0005 / 1e-6` is 500.00000000000006, and with no `digits` to round it that\n * lands in the input as written.\n */\nfunction scaleBy(value: number, scale: number): number {\n return toPrecision(value / scale)\n}\n\n/** A number, then whatever followed it. */\nconst NUMBER_THEN_REST =\n /^([+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?)\\s*(.*)$/\n\n/**\n * Build the `format` and `parse` of a unit, as one pair.\n *\n * They are returned together because they have to agree: a `format` that\n * writes `1.23kHz` is only useful next to a `parse` that reads it back as\n * 1230. Spread the result into the input.\n *\n * @example\n * unitFormat('Hz') // 1234 -> '1.23kHz'\n * unitFormat('s', { base: 'm' }) // value in ms. 1500 -> '1.5s'\n * unitFormat('s', { base: 'm', digits: 2 }) // 1500 -> '1.50s'\n * unitFormat('dB', { prefixes: false, digits: 1 }) // -6.25 -> '-6.3dB'\n *\n * @example\n * <NumberInput.Root {...unitFormat('Hz', { digits: 2 })} value={v} onChange={setV}>\n */\nexport function unitFormat(\n unit: string,\n options: UnitFormatOptions = {},\n): UnitFormatter {\n const { base = '', prefixes = true, digits, separator = '' } = options\n if (unit === '' && base !== '') {\n throw new RangeError('unitFormat: base requires a non-empty unit')\n }\n const baseScale = PREFIX_SCALE.get(base) ?? 1\n\n /**\n * `toFixed` renders anything that rounds to zero from below as `-0`, which\n * is never what a control should show.\n */\n const fixed = (value: number) => {\n const text = digits !== undefined ? value.toFixed(digits) : String(value)\n return Number(text) === 0 ? text.replace('-', '') : text\n }\n\n if (!prefixes) {\n // The stored value goes out untouched, so the symbol has to name the unit\n // it is already in.\n const symbol = base + unit\n return {\n format: (value) =>\n Number.isFinite(value)\n ? fixed(value) + separator + symbol\n : String(value),\n // Nothing after the number can change the scale, so it is all ignored:\n // the number in front is the value, half-typed or not.\n parse: (text) => {\n const match = text.trim().match(NUMBER_THEN_REST)\n if (!match) return NaN\n const value = Number(match[1])\n return Number.isFinite(value) ? value : NaN\n },\n }\n }\n\n return {\n format: (value) => {\n if (!Number.isFinite(value)) return String(value)\n const si = value * baseScale\n // Zero has no magnitude to read, so it stays in the base unit.\n let index = PREFIXES.findIndex(([, scale]) => scale === 1)\n if (si !== 0) {\n // The largest prefix that leaves at least one digit before the point.\n // Below the smallest prefix the number just gets small: `p` is the\n // floor, as `G` is the ceiling.\n const magnitude = Math.abs(si)\n index = 0\n for (let i = PREFIXES.length - 1; i >= 0; i--) {\n if (magnitude >= PREFIXES[i][1]) {\n index = i\n break\n }\n }\n }\n let text = fixed(scaleBy(si, PREFIXES[index][1]))\n // Rounding can carry the number up out of its own prefix — 999.99Hz at\n // one digit is 1000.0Hz, which should read 1.0kHz. One step is always\n // enough, since the carry is at most a factor of ten.\n if (Math.abs(Number(text)) >= 1000 && index < PREFIXES.length - 1) {\n index += 1\n text = fixed(scaleBy(si, PREFIXES[index][1]))\n }\n return text + separator + PREFIXES[index][0] + unit\n },\n\n parse: (text) => {\n const match = text.trim().match(NUMBER_THEN_REST)\n if (!match) return NaN\n const number = Number(match[1])\n if (!Number.isFinite(number)) return NaN\n\n let suffix = match[2].trim()\n const separatorText = separator.trim()\n if (separatorText !== '' && suffix.startsWith(separatorText)) {\n suffix = suffix.slice(separatorText.length).trim()\n }\n // A bare number is in the unit the value is stored in, which is what\n // the input shows once the format is stripped.\n if (suffix === '') return number\n\n // The unit symbol is matched first, so a unit that is itself a prefix\n // letter wins over the prefix reading: `5m` for a unit of `m` is five\n // metres, not five milli-.\n let prefix: string | null = null\n if (unit !== '' && suffix.endsWith(unit)) {\n prefix = suffix.slice(0, suffix.length - unit.length)\n } else if (suffix.length <= 1) {\n prefix = suffix\n }\n if (prefix === null) return number\n\n const normalized = MICRO_ALIASES[prefix] ?? prefix\n const scale = PREFIX_SCALE.get(normalized)\n // Unrecognised text after the number is ignored rather than rejected,\n // so that a half-typed entry still yields the number in front of it.\n if (scale === undefined) return number\n return (number * scale) / baseScale\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/math.ts","../src/scales.ts","../src/midi.ts","../src/unit.ts"],"sourcesContent":["/**\n * clamp value between min and max\n */\nexport function clamp(value: number, min: number, max: number) {\n return Math.max(min, Math.min(value, max))\n}\n\n/**\n * Normalize the value from 0 to 1, spreading the range evenly.\n *\n * This is the linear mapping and takes no curve of its own; a `Scale` builds\n * whatever curve it needs on top of it.\n */\nexport function normalizeValue(value: number, min: number, max: number) {\n if (min >= max) throw new RangeError('requirements: min < max')\n return clamp((value - min) / (max - min), 0, 1)\n}\n\n/**\n * Convert normalized values back to raw values, spreading the range evenly.\n *\n * The inverse of {@link normalizeValue}.\n */\nexport function rawValue(normalizedValue: number, min: number, max: number) {\n if (min >= max) throw new RangeError('requirements: min < max')\n return min + clamp(normalizedValue, 0, 1) * (max - min)\n}\n\n/**\n * Put a value on the grid the caller asked for, rounding a half step upwards.\n *\n * The rounding is done on the quotient rather than by comparing the distance\n * to the two neighbours, because both of those carry error of their own. The\n * quotient is cleared of its artefact first: `0.15 / 0.1` is 1.4999999999999998,\n * and a value sitting exactly on a half step would otherwise fall to whichever\n * side the last bit happened to land on — 0.25 rounded up while 0.15 and 0.35\n * rounded down.\n */\nexport function stepValue(value: number, step: number) {\n if (step <= 0) throw new RangeError('requirements: step > 0')\n const quotient = Math.round(toPrecision(value / step))\n // The product has its own debris to drop: 3141593 * 1e-6 is not 3.141593.\n const stepped = toPrecision(quotient * step)\n // Math.round answers -0 for anything in [-0.5, 0), and that reaches the\n // caller's onChange as a value that prints as 0 but is not it.\n return stepped === 0 ? 0 : stepped\n}\n\nexport function toFixed(x: number, fractionDigits?: number) {\n return Number(x.toFixed(fractionDigits))\n}\n\n/**\n * The significant decimal digits a double actually carries. A double holds a\n * little under 16, so anything past this is the binary representation showing\n * through rather than information.\n */\nconst SIGNIFICANT_DIGITS = 15\n\n/**\n * Drop the binary artefact from a computed value.\n *\n * Arithmetic on doubles leaves debris in the last couple of digits, and it\n * accumulates: adding 0.1 to 5 twelve times gives 5.699999999999998 rather\n * than 5.7, and the display of a control shows exactly that. Rounding to the\n * digits a double can carry removes it, and adds nothing back — the value was\n * already the result of a calculation whose own error is that size or larger.\n *\n * This is not the same as rounding to a `step`. {@link stepValue} puts a value\n * on a grid the caller asked for and is a decision about the value; this only\n * removes what was never in the value to begin with.\n *\n * @param significantDigits how many digits to keep. The default is the only\n * one that is purely artefact removal; a smaller number starts discarding real\n * precision.\n *\n * @example\n * toPrecision(5.1 + 0.1) // 5.2, rather than 5.199999999999999\n */\nexport function toPrecision(x: number, significantDigits = SIGNIFICANT_DIGITS) {\n // Zero has no significant digits to round to, and a non-finite value has no\n // decimal form to parse back.\n if (x === 0 || !Number.isFinite(x)) return x\n const rounded = Number(x.toPrecision(significantDigits))\n // Rounding up at the very top of the range overflows to Infinity, which is\n // a worse answer than the artefact.\n return Number.isFinite(rounded) ? rounded : x\n}\n\nexport function radian(degree: number) {\n return (Math.PI * degree) / 180\n}\n\nexport function degree(radian: number) {\n return (180 * radian) / Math.PI\n}\n\nexport function mapValue(\n value: number,\n inMin: number,\n inMax: number,\n outMin: number,\n outMax: number,\n) {\n return ((value - inMin) / (inMax - inMin)) * (outMax - outMin) + outMin\n}\n\nexport function dbToGain(db: number) {\n return Math.pow(10, db / 20)\n}\n\nexport function gainToDb(gain: number) {\n return 20 * (Math.log(gain) / Math.LN10)\n}\n","import { clamp, normalizeValue, rawValue } from './math'\n\n/**\n * How a value is distributed across the travel of a control.\n *\n * `normalize` and `denormalize` are inverses of each other: the position is\n * 0 at `min` and 1 at `max`, and everything in between is up to the scale.\n *\n * `min` and `max` are arguments rather than baked into the scale, so a scale\n * holds no state and can be a module level constant. Passing the same object\n * on every render therefore costs nothing.\n *\n * @example\n * ```ts\n * exponentialScale.denormalize(0.5, 20, 20000) // 632.45…\n * ```\n */\nexport interface Scale {\n /** Value to its position on the travel, 0-1. */\n normalize: (value: number, min: number, max: number) => number\n /** Position on the travel, 0-1, back to a value. */\n denormalize: (position: number, min: number, max: number) => number\n}\n\nfunction assertRange(min: number, max: number) {\n if (min >= max) throw new RangeError('requirements: min < max')\n}\n\nfunction assertPositiveFinite(value: number, name: string) {\n if (!Number.isFinite(value) || value <= 0) {\n throw new RangeError(`${name}: requirements: finite and greater than 0`)\n }\n}\n\n/**\n * Equal travel gives an equal change in value.\n *\n * The right default for anything already linear in perception: dB values,\n * pan, percentages, MIDI note numbers, semitones.\n */\nexport const linearScale: Scale = {\n normalize: (value, min, max) => normalizeValue(value, min, max),\n denormalize: (position, min, max) => rawValue(position, min, max),\n}\n\n/**\n * The power law of JUCE's `NormalisableRange::skew`, applied to `value - min`.\n *\n * Use it when the value has to agree with a JUCE or iPlug2 parameter — a\n * plugin UI in a WebView, say, where the knob must sit exactly where the\n * host's automation curve puts it. {@link skewWithCenterValue} gives the\n * factor that places a chosen value at the middle of the travel.\n *\n * `skew < 1` gives the lower end more travel, `skew > 1` the upper end.\n *\n * For new designs prefer {@link exponentialScale} or {@link curveScale}: the\n * slope of this curve is either zero or infinite at `min`, so the bottom of\n * the range is a dead zone or jumps.\n *\n * @param skew the JUCE skew factor\n */\nexport function skewScale(skew: number): Scale {\n assertPositiveFinite(skew, 'skewScale')\n return {\n // The two expressions JUCE uses, kept verbatim so the numbers agree with\n // a NormalisableRange: pow() one way, exp(log()) the other.\n normalize: (value, min, max) =>\n Math.pow(normalizeValue(value, min, max), skew),\n denormalize: (position, min, max) =>\n rawValue(\n skew === 1\n ? position\n : Math.exp(Math.log(clamp(position, 0, 1)) / skew),\n min,\n max,\n ),\n }\n}\n\n/**\n * The skew factor for {@link skewScale} that puts `centerValue` at the middle\n * of the travel — JUCE's `NormalisableRange::setSkewForCentre`.\n */\nexport function skewWithCenterValue(\n centerValue: number,\n min: number,\n max: number,\n) {\n assertRange(min, max)\n if (!(min < centerValue && centerValue < max))\n throw new RangeError('requirements: min < centerValue < max')\n return Math.log(0.5) / Math.log((centerValue - min) / (max - min))\n}\n\n/**\n * Equal travel gives an equal *ratio*, so an octave — or a percentage — takes\n * the same distance wherever it falls.\n *\n * The scale for frequency (a filter cutoff over 20-20000 Hz), free running\n * rates, and delay times.\n *\n * Requires `min` and `max` to be non-zero and of the same sign, since no\n * ratio reaches zero or crosses it. Use {@link curveScale} for a range that\n * starts at 0.\n */\nexport const exponentialScale: Scale = {\n normalize: (value, min, max) => {\n assertExponentialRange(min, max)\n // The value is clamped before the logarithm, not after: outside the range\n // the ratio can be negative, and log() would give NaN rather than a\n // position to clamp.\n const start = Math.log(Math.abs(min))\n const end = Math.log(Math.abs(max))\n return clamp(\n (Math.log(Math.abs(clamp(value, min, max))) - start) / (end - start),\n 0,\n 1,\n )\n },\n denormalize: (position, min, max) => {\n assertExponentialRange(min, max)\n const start = Math.log(Math.abs(min))\n const end = Math.log(Math.abs(max))\n const magnitude = Math.exp(start + (end - start) * clamp(position, 0, 1))\n return Math.sign(min) * magnitude\n },\n}\n\nfunction assertExponentialRange(min: number, max: number) {\n assertRange(min, max)\n if (min === 0 || max === 0 || Math.sign(min) !== Math.sign(max)) {\n throw new RangeError(\n 'exponentialScale: requirements: min and max are non-zero and have the same sign',\n )\n }\n}\n\n/**\n * An exponential bend that still passes exactly through `min` and `max`, so\n * unlike {@link exponentialScale} it works on a range that starts at 0 or\n * crosses it, and unlike {@link skewScale} its slope is neither zero nor\n * infinite at either end.\n *\n * The general purpose taper, and the same family as the curve of an envelope\n * segment (SuperCollider's `CurveWarp`).\n *\n * - `curve > 0` gives the lower end more travel — envelope times from 0 ms,\n * delay times, anything that wants fine control near the bottom\n * - `curve < 0` gives the upper end more travel — a volume fader over\n * -60..+6 dB that should be precise around 0 dB\n * - near 0 it is indistinguishable from {@link linearScale}, and is treated\n * as linear to avoid dividing by zero\n *\n * {@link curveWithCenterValue} gives the curve that places a chosen value at\n * the middle of the travel.\n *\n * @param curve how hard the curve bends, and in which direction\n */\nexport function curveScale(curve: number): Scale {\n // Beyond this the flatter half of the curve no longer has enough distinct\n // double values for normalize and denormalize to remain inverses.\n if (!Number.isFinite(curve) || Math.abs(curve) > 32) {\n throw new RangeError(\n 'curveScale: requirements: finite curve from -32 to 32',\n )\n }\n // The two coefficients blow up as the curve flattens: `a` divides by\n // 1 - e^curve, which goes to 0.\n if (Math.abs(curve) < 0.001) return linearScale\n\n return {\n normalize: (value, min, max) => {\n assertRange(min, max)\n const proportion = clamp((value - min) / (max - min), 0, 1)\n if (proportion === 0 || proportion === 1) return proportion\n if (curve > 0) {\n return (\n 1 + Math.log(proportion + (1 - proportion) * Math.exp(-curve)) / curve\n )\n }\n return Math.log1p(proportion * Math.expm1(curve)) / curve\n },\n denormalize: (position, min, max) => {\n assertRange(min, max)\n const p = clamp(position, 0, 1)\n if (p === 0) return min\n if (p === 1) return max\n const proportion =\n curve > 0\n ? (Math.exp(curve * (p - 1)) * (1 - Math.exp(-curve * p))) /\n (1 - Math.exp(-curve))\n : Math.expm1(curve * p) / Math.expm1(curve)\n return min + (max - min) * proportion\n },\n }\n}\n\n/**\n * {@link skewScale} mirrored about the middle of the range, so both halves\n * bend the same way — JUCE's `symmetricSkew`.\n *\n * For a bipolar control whose centre matters: detune over -100..+100 cents,\n * or a bipolar filter envelope amount, where the fine adjustment is around 0\n * rather than at either end.\n *\n * `skew < 1` gives the middle more travel, `skew > 1` the two ends.\n *\n * @param skew the JUCE skew factor\n */\nexport function symmetricSkewScale(skew: number): Scale {\n assertPositiveFinite(skew, 'symmetricSkewScale')\n return {\n normalize: (value, min, max) => {\n assertRange(min, max)\n const proportion = clamp((value - min) / (max - min), 0, 1)\n if (skew === 1) return proportion\n const distanceFromMiddle = 2 * proportion - 1\n return (\n (1 +\n Math.pow(Math.abs(distanceFromMiddle), skew) *\n Math.sign(distanceFromMiddle)) /\n 2\n )\n },\n denormalize: (position, min, max) => {\n assertRange(min, max)\n const p = clamp(position, 0, 1)\n let distanceFromMiddle = 2 * p - 1\n if (skew !== 1 && distanceFromMiddle !== 0) {\n distanceFromMiddle =\n Math.pow(Math.abs(distanceFromMiddle), 1 / skew) *\n Math.sign(distanceFromMiddle)\n }\n return min + ((max - min) / 2) * (1 + distanceFromMiddle)\n },\n }\n}\n\n/**\n * The curve for {@link curveScale} that puts `centerValue` at the middle of\n * the travel — the counterpart of {@link skewWithCenterValue}.\n */\nexport function curveWithCenterValue(\n centerValue: number,\n min: number,\n max: number,\n) {\n assertRange(min, max)\n if (!(min < centerValue && centerValue < max)) {\n throw new RangeError('requirements: min < centerValue < max')\n }\n // value(0.5) - min = range / (1 + e^(curve / 2))\n const proportion = (centerValue - min) / (max - min)\n return 2 * Math.log(1 / proportion - 1)\n}\n\n/**\n * How a value is scaled: the range it lives in, how it is rounded, and how it\n * is distributed across the travel.\n *\n * `AxisOptions` of `@tremolo-ui/dom` extends this, so a drag and a\n * wheel / keyboard nudge run the same value pipeline.\n */\nexport interface ValueRange {\n min: number\n max: number\n /**\n * Rounding applied to the value. Left unrounded when omitted.\n */\n step?: number\n /**\n * How the value is distributed across the travel.\n *\n * @default linearScale\n */\n scale?: Scale\n}\n","/**\n * Remainder that follows the sign of the divisor, rather than of the dividend.\n *\n * `%` in JavaScript keeps the sign of the left operand, so `-13 % 12` is `-1`\n * and cannot index a 12-element table. Every note here is placed by its\n * position within the octave, and note numbers below C-1 are negative.\n */\nfunction mod(n: number, m: number) {\n return ((n % m) + m) % m\n}\n\nexport const whiteKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] as const\n\nexport type WhiteKey = (typeof whiteKeys)[number]\n\nexport const noteKeys = [\n 'C',\n 'C#',\n 'D',\n 'D#',\n 'E',\n 'F',\n 'F#',\n 'G',\n 'G#',\n 'A',\n 'A#',\n 'B',\n] as const\n\nexport type NoteKey = (typeof noteKeys)[number]\n\nfunction assertSafeInteger(value: number, name: string) {\n if (!Number.isSafeInteger(value)) {\n throw new RangeError(`${name}: requirements: a safe integer`)\n }\n}\n\nexport function parseNoteName(noteName: string) {\n const m = noteName.match(/^([a-g])(#{0,2}|b{0,2})(-?\\d+)$/i)\n if (!m) throw new Error('Invalid note name')\n const [, letter, accidental, octave] = m\n const parsedOctave = Number(octave)\n assertSafeInteger(parsedOctave, 'octave')\n return {\n letter: letter.toLocaleUpperCase() as WhiteKey,\n accidental: accidental as '#' | '##' | 'b' | 'bb' | '',\n octave: parsedOctave,\n }\n}\n\n/**\n * Convert noteName to noteNumber\n */\nexport function noteNumber(noteName: string) {\n const { letter, accidental, octave } = parseNoteName(noteName)\n const noteIndex = noteKeys.indexOf(letter.toLocaleUpperCase() as NoteKey)\n const accidentalValue = (accidental[0] === 'b' ? -1 : 1) * accidental.length\n const result = noteIndex + 12 * (octave + 1) + accidentalValue\n assertSafeInteger(result, 'note number')\n return result\n}\n\n/**\n * Convert noteNumber to noteName\n * C-1 = 0\n * G9 = 127\n * @param noteNumber noteNumber\n */\nexport function noteName(noteNumber: number): `${NoteKey}${number}` {\n assertSafeInteger(noteNumber, 'note number')\n const noteIndex = mod(noteNumber, 12)\n const octave = Math.floor(noteNumber / 12) - 1\n return `${noteKeys[noteIndex]}${octave}`\n}\n\n/**\n * Convert noteNumber to noteKey\n */\nexport function noteKey(noteNumber: number): NoteKey {\n assertSafeInteger(noteNumber, 'note number')\n return noteKeys[mod(noteNumber, 12)]\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n */\nexport function isWhiteKey(note: number | string) {\n const n = typeof note === 'string' ? noteNumber(note) : note\n assertSafeInteger(n, 'note number')\n return (\n mod(n, 12) === 0 ||\n mod(n, 12) === 2 ||\n mod(n, 12) === 4 ||\n mod(n, 12) === 5 ||\n mod(n, 12) === 7 ||\n mod(n, 12) === 9 ||\n mod(n, 12) === 11\n )\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n */\nexport function isBlackKey(note: number | string) {\n return !isWhiteKey(note)\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n * @param detune [cent]\n * @param a4 A4 frequency [Hz]\n * @returns frequency [Hz]\n */\nexport function noteToFrequency(note: number | string, detune = 0, a4 = 440) {\n const n = typeof note === 'string' ? noteNumber(note) : note\n assertSafeInteger(n, 'note number')\n return (a4 / 32) * 2 ** ((n - 9 + detune / 100) / 12)\n}\n\n/**\n * Centre of the 14-bit pitch bend range: no bend.\n *\n * The range is not symmetric — 0 is 8192 below centre and 16383 is 8191 above\n * — so a wheel at rest reports exactly this rather than half of the maximum.\n */\nexport const PITCH_BEND_CENTER = 8192\n\n/**\n * A 14-bit pitch bend (0-16383) as -1 to 1, with 0 at rest.\n *\n * Each side of the centre is divided by its own length, since there is one\n * more step below it than above: 0 gives -1 and 16383 gives 1 exactly.\n * Dividing both sides by 8192 would never quite reach 1.\n *\n * @param value pitch bend: 0 ~ 16383\n */\nexport function normalizePitchBend(value: number) {\n const offset = value - PITCH_BEND_CENTER\n return offset / (offset < 0 ? PITCH_BEND_CENTER : PITCH_BEND_CENTER - 1)\n}\n\n/**\n * Semitones above the root, for each supported scale.\n *\n * Every entry starts at 0 and stays inside one octave, so a scale is a set of\n * pitch classes rather than a set of notes: {@link inScale} compares against\n * it with the octave taken out.\n *\n * `ionian` and `aeolian` are the same sets as `major` and `naturalMinor`; both\n * spellings are here because both are what someone reaches for depending on\n * whether they are thinking in keys or in modes.\n */\nexport const scaleIntervals = {\n major: [0, 2, 4, 5, 7, 9, 11],\n naturalMinor: [0, 2, 3, 5, 7, 8, 10],\n harmonicMinor: [0, 2, 3, 5, 7, 8, 11],\n melodicMinor: [0, 2, 3, 5, 7, 9, 11],\n\n ionian: [0, 2, 4, 5, 7, 9, 11],\n dorian: [0, 2, 3, 5, 7, 9, 10],\n phrygian: [0, 1, 3, 5, 7, 8, 10],\n lydian: [0, 2, 4, 6, 7, 9, 11],\n mixolydian: [0, 2, 4, 5, 7, 9, 10],\n aeolian: [0, 2, 3, 5, 7, 8, 10],\n locrian: [0, 1, 3, 5, 6, 8, 10],\n\n majorPentatonic: [0, 2, 4, 7, 9],\n minorPentatonic: [0, 3, 5, 7, 10],\n blues: [0, 3, 5, 6, 7, 10],\n\n wholeTone: [0, 2, 4, 6, 8, 10],\n chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],\n} as const satisfies Record<string, readonly number[]>\n\nexport type ScaleName = keyof typeof scaleIntervals\n\n/**\n * Whether a note belongs to a scale, regardless of the octave either sits in.\n *\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n * @param root the note the scale is built on, in the same two forms\n *\n * @example\n * ```ts\n * inScale('F#4', 'D3', 'major') // true\n * ```\n */\nexport function inScale(\n note: number | string,\n root: number | string,\n name: ScaleName,\n): boolean {\n const n = typeof note === 'string' ? noteNumber(note) : note\n const r = typeof root === 'string' ? noteNumber(root) : root\n assertSafeInteger(n, 'note number')\n assertSafeInteger(r, 'root note number')\n return (scaleIntervals[name] as readonly number[]).includes(mod(n - r, 12))\n}\n\n/**\n * The notes of a scale, ascending from `root`.\n *\n * The octave above the root is not included: ask for more `octaves` instead, so\n * that concatenating the result of two calls does not repeat a note.\n *\n * @param root noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n * @param octaves how many octaves to cover\n *\n * @example\n * ```ts\n * scaleNotes('C3', 'majorPentatonic') // [48, 50, 52, 55, 57]\n * ```\n */\nexport function scaleNotes(\n root: number | string,\n name: ScaleName,\n octaves = 1,\n): number[] {\n const r = typeof root === 'string' ? noteNumber(root) : root\n assertSafeInteger(r, 'root note number')\n if (!Number.isSafeInteger(octaves) || octaves < 0) {\n throw new RangeError('octaves: requirements: a non-negative safe integer')\n }\n const intervals = scaleIntervals[name] as readonly number[]\n return Array.from({ length: octaves }, (_, octave) =>\n intervals.map((interval) => r + octave * 12 + interval),\n ).flat()\n}\n","import { toPrecision } from './math'\n\n/**\n * The SI prefixes {@link unitFormat} chooses between.\n *\n * Deliberately narrower than the full SI set: yocto through yotta are of no\n * use to an audio control, and every extra prefix is one more symbol `parse`\n * has to tell apart from a unit.\n */\nexport type SIPrefix = 'p' | 'n' | 'µ' | 'm' | '' | 'k' | 'M' | 'G'\n\n/** Ordered small to large. The empty symbol is the base unit. */\nconst PREFIXES: readonly [SIPrefix, number][] = [\n ['p', 1e-12],\n ['n', 1e-9],\n ['µ', 1e-6],\n ['m', 1e-3],\n ['', 1],\n ['k', 1e3],\n ['M', 1e6],\n ['G', 1e9],\n]\n\nconst PREFIX_SCALE = new Map<string, number>(PREFIXES)\n\n/**\n * Micro is written three ways. `µ` (U+00B5 MICRO SIGN) is what `format`\n * writes and what d3-format uses, `μ` (U+03BC GREEK SMALL LETTER MU) looks\n * identical and is what a Greek keyboard produces, and `u` is what everyone\n * actually types. All three read back the same.\n */\nconst MICRO_ALIASES: Record<string, SIPrefix> = { μ: 'µ', u: 'µ' }\n\nexport interface UnitFormatOptions {\n /**\n * The prefix the stored value is already in.\n *\n * A control that keeps milliseconds in `value` is `{ base: 'm' }` with a\n * unit of `'s'`: 1500 then displays as `1.5s`, and `parse` gives 1500 back.\n *\n * @default ''\n */\n base?: SIPrefix\n /**\n * Whether to scale the number and pick a prefix at all.\n *\n * Turn it off for anything that is not an SI quantity. dB, %, cents and\n * semitones do not take prefixes, and `-6dB` read as \"-6 deci-B\" is wrong\n * rather than merely unusual.\n *\n * @default true\n */\n prefixes?: boolean\n /**\n * Digits after the decimal point. The number is left as-is when omitted.\n */\n digits?: number\n /**\n * Text placed between the number and the unit.\n * @default ''\n */\n separator?: string\n}\n\n/** The `format` / `parse` pair a `NumberInput` takes. */\nexport interface UnitFormatter {\n format: (value: number) => string\n parse: (text: string) => number\n}\n\n/**\n * Divide by a prefix scale without showing the result of doing so in binary.\n *\n * `0.0005 / 1e-6` is 500.00000000000006, and with no `digits` to round it that\n * lands in the input as written.\n */\nfunction scaleBy(value: number, scale: number): number {\n return toPrecision(value / scale)\n}\n\n/** A number, then whatever followed it. */\nconst NUMBER_THEN_REST =\n /^([+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?)\\s*(.*)$/\n\n/**\n * Build the `format` and `parse` of a unit, as one pair.\n *\n * They are returned together because they have to agree: a `format` that\n * writes `1.23kHz` is only useful next to a `parse` that reads it back as\n * 1230. Spread the result into the input.\n *\n * @example\n * unitFormat('Hz') // 1234 -> '1.23kHz'\n * unitFormat('s', { base: 'm' }) // value in ms. 1500 -> '1.5s'\n * unitFormat('s', { base: 'm', digits: 2 }) // 1500 -> '1.50s'\n * unitFormat('dB', { prefixes: false, digits: 1 }) // -6.25 -> '-6.3dB'\n *\n * @example\n * <NumberInput.Root {...unitFormat('Hz', { digits: 2 })} value={v} onChange={setV}>\n */\nexport function unitFormat(\n unit: string,\n options: UnitFormatOptions = {},\n): UnitFormatter {\n const { base = '', prefixes = true, digits, separator = '' } = options\n if (unit === '' && base !== '') {\n throw new RangeError('unitFormat: base requires a non-empty unit')\n }\n const baseScale = PREFIX_SCALE.get(base) ?? 1\n\n /**\n * `toFixed` renders anything that rounds to zero from below as `-0`, which\n * is never what a control should show.\n */\n const fixed = (value: number) => {\n const text = digits !== undefined ? value.toFixed(digits) : String(value)\n return Number(text) === 0 ? text.replace('-', '') : text\n }\n\n if (!prefixes) {\n // The stored value goes out untouched, so the symbol has to name the unit\n // it is already in.\n const symbol = base + unit\n return {\n format: (value) =>\n Number.isFinite(value)\n ? fixed(value) + separator + symbol\n : String(value),\n // Nothing after the number can change the scale, so it is all ignored:\n // the number in front is the value, half-typed or not.\n parse: (text) => {\n const match = text.trim().match(NUMBER_THEN_REST)\n if (!match) return NaN\n const value = Number(match[1])\n return Number.isFinite(value) ? value : NaN\n },\n }\n }\n\n return {\n format: (value) => {\n if (!Number.isFinite(value)) return String(value)\n const si = value * baseScale\n // Zero has no magnitude to read, so it stays in the base unit.\n let index = PREFIXES.findIndex(([, scale]) => scale === 1)\n if (si !== 0) {\n // The largest prefix that leaves at least one digit before the point.\n // Below the smallest prefix the number just gets small: `p` is the\n // floor, as `G` is the ceiling.\n const magnitude = Math.abs(si)\n index = 0\n for (let i = PREFIXES.length - 1; i >= 0; i--) {\n if (magnitude >= PREFIXES[i][1]) {\n index = i\n break\n }\n }\n }\n let text = fixed(scaleBy(si, PREFIXES[index][1]))\n // Rounding can carry the number up out of its own prefix — 999.99Hz at\n // one digit is 1000.0Hz, which should read 1.0kHz. One step is always\n // enough, since the carry is at most a factor of ten.\n if (Math.abs(Number(text)) >= 1000 && index < PREFIXES.length - 1) {\n index += 1\n text = fixed(scaleBy(si, PREFIXES[index][1]))\n }\n return text + separator + PREFIXES[index][0] + unit\n },\n\n parse: (text) => {\n const match = text.trim().match(NUMBER_THEN_REST)\n if (!match) return NaN\n const number = Number(match[1])\n if (!Number.isFinite(number)) return NaN\n\n let suffix = match[2].trim()\n const separatorText = separator.trim()\n if (separatorText !== '' && suffix.startsWith(separatorText)) {\n suffix = suffix.slice(separatorText.length).trim()\n }\n // A bare number is in the unit the value is stored in, which is what\n // the input shows once the format is stripped.\n if (suffix === '') return number\n\n // The unit symbol is matched first, so a unit that is itself a prefix\n // letter wins over the prefix reading: `5m` for a unit of `m` is five\n // metres, not five milli-.\n let prefix: string | null = null\n if (unit !== '' && suffix.endsWith(unit)) {\n prefix = suffix.slice(0, suffix.length - unit.length)\n } else if (suffix.length <= 1) {\n prefix = suffix\n }\n if (prefix === null) return number\n\n const normalized = MICRO_ALIASES[prefix] ?? prefix\n const scale = PREFIX_SCALE.get(normalized)\n // Unrecognised text after the number is ignored rather than rejected,\n // so that a half-typed entry still yields the number in front of it.\n if (scale === undefined) return number\n return (number * scale) / baseScale\n },\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tremolo-ui/functions",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "functions used in @tremolo-ui/react",
5
5
  "type": "module",
6
6
  "sideEffects": false,
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ export {
26
26
  type NoteKey,
27
27
  type ScaleName,
28
28
  type WhiteKey,
29
+ PITCH_BEND_CENTER,
29
30
  inScale,
30
31
  isBlackKey,
31
32
  isWhiteKey,
@@ -34,6 +35,7 @@ export {
34
35
  noteName,
35
36
  noteNumber,
36
37
  noteToFrequency,
38
+ normalizePitchBend,
37
39
  parseNoteName,
38
40
  scaleIntervals,
39
41
  scaleNotes,
package/src/midi.ts CHANGED
@@ -118,6 +118,28 @@ export function noteToFrequency(note: number | string, detune = 0, a4 = 440) {
118
118
  return (a4 / 32) * 2 ** ((n - 9 + detune / 100) / 12)
119
119
  }
120
120
 
121
+ /**
122
+ * Centre of the 14-bit pitch bend range: no bend.
123
+ *
124
+ * The range is not symmetric — 0 is 8192 below centre and 16383 is 8191 above
125
+ * — so a wheel at rest reports exactly this rather than half of the maximum.
126
+ */
127
+ export const PITCH_BEND_CENTER = 8192
128
+
129
+ /**
130
+ * A 14-bit pitch bend (0-16383) as -1 to 1, with 0 at rest.
131
+ *
132
+ * Each side of the centre is divided by its own length, since there is one
133
+ * more step below it than above: 0 gives -1 and 16383 gives 1 exactly.
134
+ * Dividing both sides by 8192 would never quite reach 1.
135
+ *
136
+ * @param value pitch bend: 0 ~ 16383
137
+ */
138
+ export function normalizePitchBend(value: number) {
139
+ const offset = value - PITCH_BEND_CENTER
140
+ return offset / (offset < 0 ? PITCH_BEND_CENTER : PITCH_BEND_CENTER - 1)
141
+ }
142
+
121
143
  /**
122
144
  * Semitones above the root, for each supported scale.
123
145
  *