Files
gemigreerd-racket-audio/audio-placed-player.rkt
T

504 lines
18 KiB
Racket

#lang racket/base
(require racket/place
racket/async-channel
"libao.rkt"
"audio-decoder.rkt"
"private/utils.rkt"
early-return
)
(provide placed-player
audio-known-exts?
)
(define get-current-seconds current-seconds)
(define (eq-seconds? s1 s2)
(let ((s1* (inexact->exact (round s1)))
(s2* (inexact->exact (round s2))))
(= s1* s2*)))
(define (placed-player ch-in)
(let ((ch-evt #f)
(ch-out #f)
(ao-h #f)
(ao-mutex (make-mutex))
(ao-dec #f)
(current-seconds 0)
(current-deci-seconds 0)
(stored-seconds -1)
(current-file-id 0)
(files-playing '())
(current-bits -1)
(current-rate -1)
(current-channels -1)
(current-volume 100.0)
(req-volume 100.0)
(max-buf-secs 4)
(min-buf-secs 2)
(play-thread #f)
(player-state 'stopped)
(decoder-buf-info #f)
(decoder-meta #f)
(feeding-audio #f)
(feed-interrupted #f)
)
(define-syntax with-ao-h
(syntax-rules (ao-h ao-mutex)
((_ r b1 ...)
(with-mutex ao-mutex
(if (ao-valid? ao-h)
(begin b1 ...)
r)))))
(define (put data)
(if (place-channel? ch-out)
(place-channel-put ch-out data)
(async-channel-put ch-out data)))
(define (evt data)
(if (place-channel? ch-evt)
(place-channel-put ch-evt data)
(async-channel-put ch-evt data)))
(define (get)
(if (place-channel? ch-in)
(place-channel-get ch-in)
(async-channel-get ch-in)))
(define (audio-read-worker ao-dec file-id)
(set! feeding-audio #t)
(set! play-thread
(thread (λ ()
(with-handlers ([exn:fail?
(λ (e)
(dbg-sound "Exception in audio-read-worker: ~a" e)
(set! feeding-audio #f)
(set! feed-interrupted #f)
(set! player-state 'stopped)
(evt (list 'exception (exn-message e))))])
(dynamic-wind
void
(λ ()
(dbg-sound "audio-read start")
(audio-read ao-dec)
(dbg-sound "audio-read end")
)
(λ () (set! feeding-audio #f)))
(state "audio-read-worker: just after audio-read" evt)
(if feed-interrupted
(set! feed-interrupted #f)
(begin
(evt '(audio-done))
(let ((bufsize #f))
(let loop ()
(let ((nbfs (with-ao-h 'done
(ao-bufsize-async ao-h))))
(if (eq? nbfs 'done)
(begin
(set! bufsize 0)
'done)
(cond
((eq? bufsize #f) (set! bufsize nbfs) (loop))
((= nbfs 0) (set! bufsize 0) 'done)
((> nbfs bufsize) (set! bufsize nbfs) 'done)
(else
(check-paused)
(set! bufsize nbfs)
(sleep 0.1)
(loop)))
)
)
)
;; Alleen de actuele worker mag de globale spelerstatus beëindigen.
;; Een oude drain-worker mag hoogstens zichzelf opruimen.
(when (and (= bufsize 0) (= file-id current-file-id))
(set! player-state 'stopped)
(state "audio-read-worker: after read with bufsize 0" evt))
)
)
)
)
)
)
)
)
(define (check-volume)
(unless (= req-volume current-volume)
(set! current-volume req-volume)
(with-ao-h 'no-op
(ao-set-volume! ao-h current-volume))
(state "check-volume: volume changed" evt)
)
)
(define (check-paused)
(if (eq? player-state 'paused)
(begin
(with-ao-h 'no-op (ao-pause ao-h #t))
(state "check-paused: player-state = paused" evt)
(let loop ()
(sleep 0.1)
(when (eq? player-state 'paused)
(loop)))
; If the player is no longer paused, we unpause the ao stream,
; also if the player is no longer playing (i.e. player-state = stopped)
; in which case, we expect ao-clear-async to have been executed, which
; means the playing queue is empty.
(with-ao-h 'no-op (ao-pause ao-h #f))
(state (format "check-paused: player-state = ~a" player-state) evt)
#t)
#f))
(define (audio-play type ao-type handle buf-info buffer buf-len)
(let* ((sample (hash-ref buf-info 'sample))
(rate (hash-ref buf-info 'sample-rate))
(second (/ (* sample 1.0) (* rate 1.0)))
(bits-per-sample (hash-ref buf-info 'bits-per-sample))
(bytes-per-sample (/ bits-per-sample 8))
(channels (hash-ref buf-info 'channels))
(bytes-per-sample-all-channels (* channels bytes-per-sample))
(duration (hash-ref buf-info 'duration))
)
(set! decoder-buf-info buf-info)
(when (not (and
(= current-bits bits-per-sample)
(= current-rate rate)
(= current-channels channels)))
; If we need to reopen the ao device with different bit-rates,
; we need to wait until the ao sample queue is empty
(let loop ()
(let* ((s (with-ao-h -1 (ao-at-second ao-h))))
(unless (or (= s -1) (eq-seconds? stored-seconds s))
(set! stored-seconds s)
(state "audio-play: seconds changes (III)" evt)))
(let ((bufsize (with-ao-h 0 (ao-bufsize-async ao-h))))
(if (= bufsize 0)
(with-mutex ao-mutex
(when (ao-valid? ao-h)
(ao-close ao-h))
(set! ao-h #f))
(begin (sleep 0.1) (loop)))))
)
(with-mutex ao-mutex
(when (eq? ao-h #f)
(dbg-sound "opening ao-h for ~a ~a" current-file-id files-playing)
(set! ao-h (ao-open-live bits-per-sample
rate channels
'native-endian))
)
(with-ao-h 'no-op
(ao-set-volume! ao-h current-volume)
(set! current-bits bits-per-sample)
(set! current-rate rate)
(set! current-channels channels)
)
)
(check-volume)
(with-ao-h 'no-op
(when (not (eq? player-state 'stopped))
(ao-play ao-h current-file-id second duration buffer buf-len ao-type)
))
(check-paused)
(let* ((s* (with-ao-h 0 (ao-at-second ao-h)))
(s (inexact->exact (round (* s* 10)))))
(unless (= s current-deci-seconds)
(set! current-deci-seconds s)
(set! current-seconds s*)))
(unless (eq-seconds? stored-seconds current-seconds)
(set! stored-seconds current-seconds)
(state "audio-play: seconds changed (I)" evt))
(let* ((buf-size (with-ao-h 0 (ao-bufsize-async ao-h)))
(buf-seconds (exact->inexact (/ buf-size
bytes-per-sample-all-channels
rate)))
)
(when (> buf-seconds max-buf-secs)
(let waiter ()
(when (not (check-paused))
(sleep 0.3))
(let* ((s* (with-ao-h 0 (ao-at-second ao-h)))
(s (inexact->exact (round (* s* 10)))))
(unless (= s current-deci-seconds)
(set! current-deci-seconds s)
(set! current-seconds s*))
(unless (eq-seconds? stored-seconds current-seconds)
(set! stored-seconds current-seconds)
(state "audio-play: seconds changed (II)" evt))
(let ((buf-seconds-left (exact->inexact
(/ (with-ao-h 0 (ao-bufsize-async ao-h))
bytes-per-sample-all-channels
rate))))
(when (>= buf-seconds-left min-buf-secs)
(waiter)))))
))
)
)
(define (audio-meta type ao-type handle meta)
(set! decoder-meta meta)
#t)
(define (cleanup)
(set! files-playing '())
(set! current-seconds 0)
(set! current-deci-seconds 0)
(set! stored-seconds -1)
(set! current-file-id 0)
(set! current-bits -1)
(set! current-rate -1)
(set! current-channels -1)
(set! decoder-buf-info #f)
(set! decoder-meta #f)
)
(define (stop-and-cleanup)
(dbg-sound "stop and cleanup called")
(set! feed-interrupted #t)
(with-ao-h 'no-op (ao-clear-async ao-h))
(set! player-state 'stopped)
(unless (eq? ao-dec #f)
(audio-stop ao-dec))
(set! ao-dec #f)
(with-ao-h 'no-op (ao-clear-async ao-h))
(when (thread? play-thread) (thread-wait play-thread))
(set! play-thread #f)
(with-mutex ao-mutex
(when (ao-valid? ao-h)
(ao-close ao-h)
)
(set! ao-h #f))
(set! feed-interrupted #f)
(set! feeding-audio #f)
(cleanup)
(state "stop-and-cleanup: stopped/cleaned" evt 'force)
player-state
)
(define (start file)
(dbg-sound "starting ~a" file)
(when feeding-audio
(dbg-sound "interrupting feed")
(set! feed-interrupted #t)
(with-ao-h 'no-op (ao-clear-async ao-h))
(set! player-state 'stopped)
(when (audio-handle? ao-dec)
(audio-stop ao-dec))
(dbg-sound "clearing ao-h queue")
(with-ao-h 'no-op (ao-clear-async ao-h))
(dbg-sound "waiting for feed to stop")
(let loop ()
(if feeding-audio
(begin
(sleep 0.1)
(loop))
(with-ao-h 'no-op (ao-clear-async ao-h))
)
)
(dbg-sound "waiting for play thread")
(when (thread? play-thread) (thread-wait play-thread))
(dbg-sound "oke done")
)
;(set! current-file-id (+ current-file-id 1))
(set! current-file-id (+ (* (get-current-seconds) 10000) (random 1000)))
(let ((f (build-path file)))
(set! files-playing (cons
(cons current-file-id f)
(filter (λ (e)
(= (car e) (- current-file-id 1)))
files-playing))))
(set! ao-dec (audio-open file audio-meta audio-play))
(when (eq? player-state 'stopped)
(set! player-state 'playing))
(audio-read-worker ao-dec current-file-id)
current-file-id)
(define (pause paused)
(when (or (eq? player-state 'paused)
(eq? player-state 'playing))
(set! player-state (if paused 'paused 'playing))))
(define (seek percentage)
(with-ao-h 'no-op
(ao-clear-async ao-h))
(unless (eq? ao-dec #f)
(audio-seek ao-dec percentage)))
(define (volume percentage)
(set! req-volume percentage))
(define (ao-buf-ms)
(ao-playback-buf-ms))
(define (ao-buf-ms! ms)
(let ((the-ms (if (< ms 50) 50 (if (> ms 1000) 1000 ms))))
(ao-set-playback-buf-ms! the-ms)
(ao-buf-ms)))
(define (state msg cb . force)
(let ((h (make-hash)))
(with-mutex ao-mutex
(let ((m-id (if (ao-valid? ao-h) (ao-at-music-id ao-h) #f)))
(hash-set! h 'decoder (if (audio-handle? ao-dec) (audio-kind ao-dec) #f))
(hash-set! h 'msg msg)
(hash-set! h 'file (let ((r (filter (λ (e)
(and (not (eq? m-id #f)) (= (car e) m-id)))
files-playing)))
(if (null? r) #f (cdar r))))
(hash-set! h 'state player-state)
(hash-set! h 'valid-ao-handle (ao-valid? ao-h))
(hash-set! h 'duration (if (ao-valid? ao-h) (ao-music-duration ao-h) #f))
(hash-set! h 'at-second (if (ao-valid? ao-h) (ao-at-second ao-h) #f))
(hash-set! h 'at-music-id m-id)
(hash-set! h 'volume current-volume)
(hash-set! h 'buf-size (if (ao-valid? ao-h) (ao-bufsize-async ao-h) 0))
(hash-set! h 'reuse-buf-len (if (ao-valid? ao-h)
(ao-reuse-buf-len-async ao-h)
#f))
(hash-set! h 'sample-queue-len (if (ao-valid? ao-h)
(ao-sample-queue-len-async ao-h)
#f))
(hash-set! h 'bits current-bits)
(hash-set! h 'rate current-rate)
(hash-set! h 'channels current-channels)
(hash-set! h 'decoder-meta decoder-meta)
(hash-set! h 'decoder-buf-info decoder-buf-info)
)
)
(let ((m-id (hash-ref h 'at-music-id)))
(unless (and (null? force) (or (eq? m-id #f) (= m-id 0)))
(cb (list 'state h))))
)
)
(let loop ()
(let* ((data (get))
(cmd (car data))
(in-rpc #f))
(define-syntax do-rpc
(syntax-rules (in-rpc)
((_ b1 ...)
(begin
(set! in-rpc #t)
(let ((r (begin b1 ...)))
(set! in-rpc #f)
(put r))))))
(with-handlers ([exn:fail? (λ (e)
(if (eq? ch-evt #f)
(raise e)
(begin
(evt (list 'exception
(exn-message e)))
(when in-rpc
(put (list 'error
(exn-message e)))
(set! in-rpc #f))
(loop))
))])
(cond
((eq? cmd 'quit) (do-rpc
(stop-and-cleanup)
(set! player-state 'quit)
(state "quit" evt 'force)
'(quit)))
((eq? cmd 'init) (do-rpc
(set! ch-out (cadr data))
(set! ch-evt (caddr data))
'(initialized))
(loop))
(else
(when (or (eq? ch-out #f) (eq? ch-evt #f))
(error "placed player not initialized"))
(unless (eq? cmd 'quit)
(cond
((eq? cmd 'buf-seconds)
(do-rpc
(let* ((clamp (λ (x) (min 30 (max 2 x))))
(a (clamp (cadr data)))
(b (clamp (caddr data))))
(set! min-buf-secs (min a b))
(set! max-buf-secs (max a b))
'(ok))))
((eq? cmd 'open)
(do-rpc
(let ((file (cadr data)))
(let ((id (start file)))
(list (list 'ok id))))))
((eq? cmd 'seek)
(do-rpc
(let ((percentage (cadr data)))
(seek percentage)
'(ok))))
((eq? cmd 'pause)
(do-rpc
(let ((paused (cadr data)))
(pause paused)
'(ok))))
((eq? cmd 'paused)
(do-rpc
(list (eq? player-state 'paused))))
((eq? cmd 'volume)
(do-rpc
(let ((percentage (cadr data)))
(volume (exact->inexact percentage))
'(ok))))
((eq? cmd 'get-volume)
(do-rpc
(list current-volume)))
((eq? cmd 'stop)
(do-rpc
(stop-and-cleanup)
'(ok)))
((eq? cmd 'state)
(do-rpc
(let ((st #f))
(state "'state command" (λ (s) (set! st s)) 'force)
st)))
((eq? cmd 'ao-buf-ms)
(do-rpc
(if (null? (cdr data))
(list (ao-buf-ms))
(list (ao-buf-ms! (cadr data))))
))
(else
(do-rpc
(list 'error (format "Unknown command ~a" cmd))))
)
(loop)
)
)
)
)
)
)
)
)