All files / web/src/components/toys/euclid/audio pitchMapping.ts

0% Statements 0/75
0% Branches 0/1
0% Functions 0/1
0% Lines 0/75

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76                                                                                                                                                       
/**
 * Geometric-to-musical conversion for Euclid construction music.
 * Pure functions, no side effects.
 *
 * Uses D minor pentatonic scale (D F G A C) across octaves 2-6.
 * Shorter segments -> higher pitch (like shorter strings).
 */

const SCALE_NOTES = ['d', 'f', 'g', 'a', 'c'] as const
const MIN_OCTAVE = 2
const MAX_OCTAVE = 6

// Build full scale array from low to high
const FULL_SCALE: string[] = []
for (let oct = MIN_OCTAVE; oct <= MAX_OCTAVE; oct++) {
  for (const note of SCALE_NOTES) {
    FULL_SCALE.push(`${note}${oct}`)
  }
}

const CENTER_INDEX = Math.floor(FULL_SCALE.length / 2)

/**
 * Map a geometric distance to a note in the D minor pentatonic scale.
 *
 * Logarithmic mapping: shorter segments -> higher pitch (like shorter strings).
 * Reference distance maps to scale center. Equal distances always produce
 * the same note -- the key musical-geometric link.
 */
export function distanceToNote(distance: number, refDistance: number): string {
  if (distance <= 0 || refDistance <= 0) return FULL_SCALE[CENTER_INDEX]

  // Log ratio: ref/distance -> shorter = positive -> higher pitch
  const logRatio = Math.log2(refDistance / distance)
  const offset = Math.round(logRatio * SCALE_NOTES.length)
  const index = Math.max(0, Math.min(FULL_SCALE.length - 1, CENTER_INDEX + offset))

  return FULL_SCALE[index]
}

/**
 * Generate arpeggio notes for a circle based on its radius.
 * Returns a Strudel mini-notation string (e.g. "f3 g3 a3 c4").
 * 4 notes spanning the scale around the radius-derived pitch.
 */
export function circleArpNotes(radius: number, refDistance: number): string {
  if (radius <= 0 || refDistance <= 0) return FULL_SCALE[CENTER_INDEX]

  const logRatio = Math.log2(refDistance / radius)
  const offset = Math.round(logRatio * SCALE_NOTES.length)
  const centerIdx = Math.max(2, Math.min(FULL_SCALE.length - 2, CENTER_INDEX + offset))

  const indices = [centerIdx - 1, centerIdx, centerIdx + 1, centerIdx + 2].map((i) =>
    Math.max(0, Math.min(FULL_SCALE.length - 1, i))
  )

  return indices.map((i) => FULL_SCALE[i]).join(' ')
}

/**
 * Map circle radius to a Strudel .slow() factor.
 * Larger radius = slower arpeggio (proportional to circumference).
 */
export function radiusToSlowFactor(radius: number, refDistance: number): number {
  if (radius <= 0 || refDistance <= 0) return 4
  return Math.max(1, Math.min(16, (radius / refDistance) * 4))
}

/**
 * Map center X coordinate to Strudel pan value (0-1, where 0.5 is center).
 */
export function centerXToPan(x: number, minX: number, maxX: number): number {
  if (maxX <= minX) return 0.5
  return Math.max(0, Math.min(1, (x - minX) / (maxX - minX)))
}