yue

Programmatic music library

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
  84. 84
  85. 85
  86. 86
  87. 87
  88. 88
  89. 89
  90. 90
  91. 91
  92. 92
  93. 93
  94. 94
  95. 95
  96. 96
  97. 97
  98. 98
  99. 99
  100. 100
  101. 101
  102. 102
  103. 103
  104. 104
  105. 105
  106. 106
  107. 107
  108. 108
  109. 109
  110. 110
  111. 111
  112. 112
  113. 113
  114. 114
  115. 115
  116. 116
  117. 117
  118. 118
  119. 119
  120. 120
import std/[algorithm, math, sugar, strformat, logging]

type
    OscFn* = proc (f: float, t: float): float

    Note* = tuple
        len: float ## seconds
        freq: float
        vol: float
        osc: OscFn

    ProcessedNote* = tuple
        start: float ## absolute time in seconds
        stop: float  ## absolute time in seconds
        freq: float
        vol: float
        osc: OscFn

const HACK_LONGEST_NOTE = 16.0

func process*(music: var seq[ProcessedNote], notes: openArray[Note]; start_init: float, speed: float=1) =
    ## Adds a list of notes to the music list
    ##
    ## `notes`  sequence of notes with no rests in between
    var start = start_init
    var t = start
    for note in notes:
        assert note.len >= 0.0
        assert note.len <= HACK_LONGEST_NOTE, &"note too long: {note.len}"
        start = t
        let stop = t + note.len / speed
        music &= (start, stop, note.freq, note.vol, note.osc)
        t = stop

func sortByStart*(music: var seq[ProcessedNote]) =
    music.sort((a, b) => cmp(a.start, b.start))

func bisect(music: openArray[ProcessedNote], x: float): int =
    ## Return the index where to insert item `x` in list `music`
    ## 
    ## assumes `music` is sorted by `.start`

    music.lowerBound(x, (m, key) => cmp(m.start, key))

const GAIN_BIAS: float = pow(2.0, 31.0)

proc at*(music: openArray[ProcessedNote], t: float): int32 =
    ## Returns the total intensity of music sampled at time t
    ## 
    ## assumes `music` is sorted by `.start`

    var i: int = music.bisect(t) - 1    
    
    var ret: float = 0

    while i >= 0:
        let m = music[i]
        assert m.start <= t
        if m.start + HACK_LONGEST_NOTE < t:
            break
        else:
            ret += m.vol * m.osc(m.freq, t - m.start)
        i -= 1

    ret *= GAIN_BIAS
    
    # clip sample
    if ret >= int32.high.float:
        warn(&"audio clipping at t={t}")
        int32.high
    elif ret <= int32.low.float:
        warn(&"audio clipping at t={t}")
        int32.low
    else:
        int32(ret)

func osc_weird_pluck*(f, t: float): float =
  # I got this as a bug
  let w = 2 * PI * f
  let wxt = w * t
  let exp_swxt = math.exp(-0.001 * wxt)
  let y0 = 0.6 * math.sin(wxt)
  let y1 = 0.2 * math.sin(2 * wxt)
  let y2 = 0.05 * math.sin(3 * wxt)
  let y3 = (y0 + y1 + y2) * exp_swxt
  let y4 = (1+y3)*y3*y3 # this line is different
  y4 * (1 + 16 * t * math.exp(-6 * t))

func osc_piano*(f, t: float): float =
  ## Returns the intensity of a tone of frequency f sampled at time t
  ## t starts at 0 (note start)
  # https://dsp.stackexchange.com/questions/46598/mathematical-equation-for-the-sound-wave-that-a-piano-makes
  # https://youtu.be/ogFAHvYatWs?t=254
  let w = 2 * PI * f
  let ewt = math.exp(-0.001 * w * t)
  var Y = 0.6 * math.sin(w * t) * ewt +
        0.2 * math.sin(2 * w * t) * ewt +
        0.05 * math.sin(3 * w * t) * ewt

  let Y2 = Y * (Y * Y + 1)
  Y2 * (1 + 16 * t * math.exp(-6 * t))

func osc_pulse*(f, t: float, phasedrift: float = 0.0): float =
  let doublewidth = 1.0 / f
  let width = doublewidth / 2
  let phase: float = (t + doublewidth * phasedrift) mod doublewidth
  if phase < width: 1.0 else: -1.0

func osc_saw*(f,t:float, phasedrift: float = 0.0):float =
  let doublewidth = 1.0 / f
  let width = doublewidth / 2
  let phase: float = (t + doublewidth * phasedrift) mod doublewidth
  if phase < width:
    -1.0 + 2.0 * phase / width
  else:
    1.0 - 2.0 * (phase - width) / (1.0 - width)

func freq*(octave, step: float): float =
  ## Returns the frequency of a note
  55 * pow(2, (octave + step / 12 - 1))