portaudio-utils.rkt
#lang racket

(require ffi/vector
         ffi/unsafe
         (rename-in racket/contract [-> c->])
         racket/runtime-path)

(define-runtime-path lib "lib/")

(define (frames? n)
  (and (exact-integer? n)
       (<= 0 n)))



(provide/contract 
 [make-sndplay-record (c-> s16vector? cpointer?)]
 [copying-callback cpointer?]
 [stop-sound (c-> cpointer? void?)]
 #;[make-generating-callback (c-> procedure? frames? channel? box? any)]
 #;[make-block-generating-callback (c-> procedure? channel? box? any)])

;; all of these functions assume 2-channel-interleaved 16-bit input:
(define channels 2)
(define s16max 32767)
(define s16-bytes 2)

;; COPYING CALLBACKS

(define-cstruct _rack-audio-closure
  ([sound         _pointer]
   [curSample     _ulong]
   [numSamples    _ulong]
   [stop-now      _bool]
   [stop-sema-ptr _pointer]))

;; create a fresh rack-audio-closure structure, including a full
;; malloc'ed copy of the sound data
(define (make-sndplay-record s16vec)
  (define finished-semaphore (make-semaphore))
  ;; will never get freed....
  ;; commenting this out until it stops seg faulting
  #;(define immobile-cell (malloc-immobile-cell finished-semaphore))
  (define closure
    (create-closure/raw (s16vector->cpointer s16vec) 
                        (s16vector-length s16vec)
                        #f #;immobile-cell))
  (unless closure
    (error 'create-copying-closure
           "failed to allocate space for ~s samples."
           (s16vector-length s16vec)))
  (hash-set! sound-stopping-table closure (make-semaphore 1))
  closure)

;; stop a sound
;; EFFECT: stops the sound *and frees the sndplay-record*, unless
;; it's already done.
(define (stop-sound sndplay-record)
  (match (hash-ref sound-stopping-table sndplay-record #f)
    [#f (error 'stop-sound "record had no entry in the stopping table")]
    [sema (match (semaphore-try-wait? sema)
            ;; sound has already been stopped
            [#f (void)]
            [#t (set-rack-audio-closure-stop-now! sndplay-record #t)])]))

;; the library containing the C copying callbacks
(define copying-callbacks-lib (ffi-lib (build-path lib
                                                   (system-library-subpath)
                                                   "copying-callbacks")))

;; in order to get a raw pointer to pass back to C, we declare
;; the function pointer as being a simple struct:
(define-cstruct _bogus-struct
  ([datum _uint16]))

(define copying-callback
  (get-ffi-obj "copyingCallback"
               copying-callbacks-lib _bogus-struct))

(define create-closure/raw
  (get-ffi-obj "createClosure" copying-callbacks-lib
               (_fun _pointer _ulong _pointer -> _rack-audio-closure-pointer)))


(define sound-stopping-table (make-weak-hash))




#|
;; create a callback that creates frames by calling a signal repeatedly.
;; note that we don't bother checking to see whether the buffer is successfully
;; filled.
(define (make-generating-callback signal buffer-frames response-channel abort-box)
  (define signal-exn-box (box #f))
  (define-values (filled-buffer semaphore)
    (start-filler-thread signal buffer-frames signal-exn-box abort-box))
  (define buffer-samples (* buffer-frames channels))
  ;; allow the first buffer-fill to happen:
  (semaphore-post semaphore)
  (define callback-holding-box (box #f))
  (define (the-callback input output frame-count time-info status-flags user-data)
    ;; the following code is believed not to be able to raise any errors.
    ;; if this is wrong, racket will die with abort().
    
    ;; don't run if abort is set:
    (cond [(unbox abort-box)
           (define response
             (or (unbox signal-exn-box) 'finished))
           (channel-put/async response-channel response)
           (set-box! callback-holding-box #f)
           'pa-abort]
          ;; don't run if we get the wrong number of frames requested:
          [(not (= frame-count buffer-frames))
           (channel-put/async
            response-channel
            (exn:fail
             (format
              "make-generating-callback: callback wanted ~s frames instead of expected ~s." 
              frame-count
              buffer-frames)
             (current-continuation-marks)))
           (set-box! callback-holding-box #f)
           'pa-abort]
          ;; otherwise, copy and release the semaphore to generate again.
          [else
           (memcpy output
                   0
                   filled-buffer
                   0
                   buffer-samples
                   _sint16)
           (semaphore-post semaphore)
           'pa-continue]))
  (set-box! callback-holding-box the-callback)
  the-callback)


;; this thread is not run as a callback. That way, if the user's signal
;; misbehaves, it won't destroy the audio engine. It fills 
;; the output buffer once for each post to the semaphore.
(define (start-filler-thread signal buffer-frames signal-exn-box abort-box)
  (define signal-semaphore (make-semaphore))
  (define buffer (make-s16vector (* channels buffer-frames)))
  (define cpointer (s16vector->cpointer buffer))
  (define frame-offset 0)
  (thread
   (lambda ()
     (let loop ()
       (semaphore-wait signal-semaphore)
       (with-handlers ([(lambda (exn) #t)
                        (lambda (exn)
                          (set-box! signal-exn-box exn)
                          (set-box! abort-box #t)
                          ;; fall off the end of the thread:
                          (void))])
         (for ([t (in-range frame-offset (+ frame-offset buffer-frames))]
               [i (in-range 0 (* 2 buffer-frames) 2)])
           (define sample 
             (inexact->exact 
              (round (* s16max (min 1.0 (max -1.0 (signal t)))))))
           (ptr-set! cpointer _sint16 i sample)
           (ptr-set! cpointer _sint16 (+ i 1) sample))
         (set! frame-offset (+ frame-offset buffer-frames))
         (loop)))))
  (values cpointer signal-semaphore))

;; create a callback that creates frames by passing a cblock to a function
(define (make-block-generating-callback signal/block/s16 response-channel abort-box)
  (define frame-offset 0)
  (define callback-holding-box (box #f))
  (define (the-callback input output frame-count time-info status-flags user-data)
    (cond [(unbox abort-box) 'pa-abort]
          ;; MUST NOT ALLOW AN EXCEPTION TO ESCAPE.
          [else (with-handlers ([(lambda (exn) #t)
                                 (lambda (exn)
                                   (channel-put/async response-channel exn)
                                   'pa-abort)])
                  (define keep-running? 
                    (signal/block/s16 output frame-offset frame-count))
                  (set! frame-offset (+ frame-offset frame-count))
                  (cond [keep-running? 'pa-continue]
                        [else (channel-put/async response-channel 'finished)
                              (set-box! callback-holding-box #f)
                              'pa-complete]))]))
  (set-box! callback-holding-box the-callback)
  the-callback)

;; we need to hold on to the callbacks, so they don't get collected while
;; the streams are still being processed.
(define callback-boxes (list))


|#