;; vosim.sal -- Vosim demo
;;
;; Roger B. Dannenberg
;; March, 2013, revised March 2015
;;
;; This is a simple program intended to demonstrate Vosim. 
;; It could form the basis for a more capable implementation.
;; Uncomment the calls to test1, test2, and test3, at the end,
;; or just run "exec test1()" etc. to hear some Vosim sounds.

;; "Real" Vosim is a series of sine-squared pulses of decreasing
;; amplitude. This requires sub-sample spacing of pulses to get 
;; good frequency control, so instead, I make a continuous 
;; sine-squared signal and multiply by a decreasing exponential

;; Vosim "grains" should start on sub-sample boundaries, but this 
;; is tricky too, so I  will instead use a granular synthesis 
;; approach. Nyquist will quantize each grain to a sample boundary,
;; so there will be some frequency jitter. My guess is this will 
;; make the result slightly richer, so it might be a "feature" 
;; rather than a bug.

;; The parameters are:
;;   hz: the fundamental frequency
;;   amp: the peak amplitude
;;   fhz: the "formant" frequency
;;   decay: an amplitude scale factor applied to each successive
;;          pulse to achieve an exponential decay
;;   count: the number of pulses
;; Unlike "true" Vosim, the period is controlled with the hz 
;; parameter rather than simply summing up the durations of the
;; pulses and adding a delay.

;; The computation is as follows:
;;   vosim() computes a sequence of grains. The number of grains
;; is estimated using the initial value of the hz parameter and
;; calculating how many periods are needed to obtain the duration
;; (duration comes from get-duration(1) as usual). Thus, the 
;; actual duration might not match get-duration(1) if hz is a 
;; signal that increases or decreases.
;;   vosim-grain() computes each grain. Grains may overlap (as in
;; FOF synthesis -- again, this is not exactly like the original
;; Vosim). Each grain is a sequence of fixed-frequency 
;; sine-squared pulses. The frequency is the current value of fhz.
;; There are count sine-squared pulses. They are scaled by an
;; exponential envelope that decreases to a factor of decay every
;; pulse period. E.g. if decay = 0.6, then each pulse will be 0.6
;; times the height of the previous pulse. Unlike true Vosim, the 
;; decay is continuous rather than taking a step every pulse 
;; period.
;;   The hz and fhz controls may be SOUNDS, but amp, decay, and 
;; count are numbers, and count must be a FIXNUM (integer).

function vosim-grain(hz, amp, fhz, decay, count)
  begin
    with fperiod = #?(numberp(fhz), 1.0 / fhz, 1.0 / sref(fhz, 0)),
         period = #?(numberp(hz), 1.0 / hz, 1.0 / sref(hz, 0)),
         dur = fperiod * count,
         f = hzosc(fhz * 0.5) ~~ dur, ;; sine-squared doubles fhz
         final-amp = power(decay, count) * amp,
         decay-env = pwev(amp, dur, final-amp),
         rslt = set-logical-stop(f * f * decay-env, period)
    ;display "vosim-grain", period, fperiod, snd-length(rslt, 100000)
    return rslt
  end

; exec s-plot(vosim-grain(50, 0.9, 1000, 0.9, 20))

function vosim(hz, amp, fhz, decay, count)
  begin
    with hz-estimate = #?(numberp(hz), hz, sref(hz, 0))
    return seqrep(i, round(get-duration(1) * hz-estimate),
                  vosim-grain(hz, amp, fhz, decay, count) ~~ 1.0)
  end

;exec s-plot(vosim(100, 0.9, 1000, 0.8, 5) ~ 0.03)
;exec s-plot(vosim(100, 0.9, pwlv(1000, 1, 4000), 0.8, 5) ~ 0.03)  

function test1()
  play vosim(100, 0.9, pwlv(200, 0.5, 4000, 1, 200), 0.8, 5) ~ 4

function test2() ;; try "oo" with formants at 300, 890
  begin
    with freq = 50 + lfo(4, 4) * 0.5
    play (pwl(0.03, 1, 0.9, 1, 1) * 
          (vosim(freq, 0.9, 300, 0.95, 20) + 
           vosim(freq, 0.9, 890, 0.95, 20))) ~ 4
  end

function test3() ;; just 890 Hz formant sounds a bit like "ah"
  begin
    with freq = 50 + lfo(4, 4) * 0.5
    play (pwl(0.03, 1, 0.9, 1, 1) * 
          (vosim(freq, 0.9, 890, 0.8, 10))) ~ 4
  end

;exec test1()
;exec test2()
;exec test3()