(module ffmpeg_ffi racket/base (require ffi/unsafe ffi/unsafe/define ffi/unsafe/alloc "private/utils.rkt" ) (provide fmpg-ffi-decoder-handler fmpg-version ) ;; The native shim is the new instance-only FFmpeg audio API. It exposes a ;; single opaque fmpg_instance pointer and keeps stream-index, packets, ;; decoder state and file metadata inside that instance. The public C header ;; says that output is always signed 32-bit interleaved PCM and that the ;; current buffer pointer is valid until the next decode/seek/close/free call. ;; Adjust the names below if your shared library has another basename. ;; get-lib is used in the same style as libmpg123-ffi.rkt. (when (eq? (system-type 'os) 'windows) ; preload ffmpeg dlls. (void (begin (get-lib '("avutil-60.dll") '(#f)) (get-lib '("swresample-6.dll") '(#f)) (get-lib '("avcodec-62") '(#f)) (get-lib '("avformat-62.dll") '(#f)) ) ) ) (define lib (get-lib '("ffmpeg_audio" "libffmpeg_audio") '(#f))) (define-ffi-definer define-ffmpeg-audio lib #:default-make-fail make-not-available) (define _fmpg_instance _pointer) (define (fmpg-version) (let* ((v (fmpg_version)) (patch (remainder v 256)) (minor (remainder (quotient v 256) 256)) (major (quotient v 65536)) ) (list major minor patch))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Native bindings ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define-ffmpeg-audio fmpg_init (_fun -> _fmpg_instance)) (define-ffmpeg-audio fmpg_free (_fun _fmpg_instance -> _void)) (define-ffmpeg-audio fmpg_open_file (_fun _fmpg_instance _string/utf-8 -> _int)) (define-ffmpeg-audio fmpg_close (_fun _fmpg_instance -> _void)) (define-ffmpeg-audio fmpg_is_open (_fun _fmpg_instance -> _int)) (define-ffmpeg-audio fmpg_audio_stream_count (_fun _fmpg_instance -> _int)) (define-ffmpeg-audio fmpg_audio_sample_rate (_fun _fmpg_instance -> _int)) (define-ffmpeg-audio fmpg_audio_channels (_fun _fmpg_instance -> _int)) (define-ffmpeg-audio fmpg_audio_bits_per_sample (_fun _fmpg_instance -> _int)) (define-ffmpeg-audio fmpg_audio_bytes_per_sample (_fun _fmpg_instance -> _int)) (define-ffmpeg-audio fmpg_duration_ms (_fun _fmpg_instance -> _int64)) (define-ffmpeg-audio fmpg_duration_samples (_fun _fmpg_instance -> _int64)) (define-ffmpeg-audio fmpg_file_title (_fun _fmpg_instance -> _string/utf-8)) (define-ffmpeg-audio fmpg_file_author (_fun _fmpg_instance -> _string/utf-8)) (define-ffmpeg-audio fmpg_file_album (_fun _fmpg_instance -> _string/utf-8)) (define-ffmpeg-audio fmpg_file_genre (_fun _fmpg_instance -> _string/utf-8)) (define-ffmpeg-audio fmpg_file_comment (_fun _fmpg_instance -> _string/utf-8)) (define-ffmpeg-audio fmpg_file_copyright (_fun _fmpg_instance -> _string/utf-8)) (define-ffmpeg-audio fmpg_file_year (_fun _fmpg_instance -> _int)) (define-ffmpeg-audio fmpg_file_track (_fun _fmpg_instance -> _int)) (define-ffmpeg-audio fmpg_file_bitrate (_fun _fmpg_instance -> _int)) (define-ffmpeg-audio fmpg_decode_next (_fun _fmpg_instance -> _int)) (define-ffmpeg-audio fmpg_seek_ms (_fun _fmpg_instance _int64 -> _int)) (define-ffmpeg-audio fmpg_buffer (_fun _fmpg_instance -> _pointer)) (define-ffmpeg-audio fmpg_buffer_size (_fun _fmpg_instance -> _int)) (define-ffmpeg-audio fmpg_buffer_samples (_fun _fmpg_instance -> _int64)) (define-ffmpeg-audio fmpg_buffer_start_sample (_fun _fmpg_instance -> _int64)) (define-ffmpeg-audio fmpg_buffer_end_sample (_fun _fmpg_instance -> _int64)) (define-ffmpeg-audio fmpg_sample_position (_fun _fmpg_instance -> _int64)) (define-ffmpeg-audio fmpg_timecode (_fun _fmpg_instance -> _double)) (define-ffmpeg-audio fmpg_ffmpeg_version (_fun -> _string*/utf-8)) (define-ffmpeg-audio fmpg_int_version2string (_fun _int -> _string*/utf-8)) (define-ffmpeg-audio fmpg_compatible_ffmpeg (_fun -> _int)) (define-ffmpeg-audio fmpg_version (_fun -> _int)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Our interface for decoding to racket ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (ok? r) (not (= r 0))) (define (str v) (if (string? v) v "")) (define (known-int64? v) (and (integer? v) (not (= v -1)))) (define (copy-current-buffer fh) (let ((size (fmpg_buffer_size fh))) (cond ((<= size 0) (values #f 0)) (else (let ((src (fmpg_buffer fh))) (if (eq? src #f) (error (format "fmpg_buffer: got NULL for ~a bytes" size)) ;(let ((dst (malloc size 'nonatomic))) (let ((dst (make-bytes size))) (memcpy dst src size) (values dst size)))))))) (define (fmpg-ffi-decoder-handler) (define fh #f) (define rate -1) (define channels -1) (define sample-bits -1) (define sample-bytes -1) (define pcm-length -1) (define duration-ms -1) (define audio-streams -1) (define ffmpeg-file "") (define current-pcm-pos 0) (define title "") (define author "") (define album "") (define genre "") (define comment "") (define copyright "") (define year -1) (define track -1) (define bitrate -1) (define (new) (if (eq? fh #f) (begin (set! fh (fmpg_init)) (when (eq? fh #f) (error "fmpg_init: could not allocate ffmpeg instance")) #t) (error "ffmpeg handle already initialized, delete it first"))) (define (delete) (if (eq? fh #f) (error "ffmpeg handle has already been deleted") (begin (fmpg_free fh) (set! fh #f) (set! rate -1) (set! channels -1) (set! sample-bits -1) (set! sample-bytes -1) (set! pcm-length -1) (set! duration-ms -1) (set! audio-streams -1) (set! ffmpeg-file "") (set! current-pcm-pos 0) #t))) (define (fetch-info) (set! rate (fmpg_audio_sample_rate fh)) (set! channels (fmpg_audio_channels fh)) (set! sample-bits (fmpg_audio_bits_per_sample fh)) (set! sample-bytes (fmpg_audio_bytes_per_sample fh)) (set! pcm-length (fmpg_duration_samples fh)) (set! duration-ms (fmpg_duration_ms fh)) (set! audio-streams (fmpg_audio_stream_count fh)) (set! title (str (fmpg_file_title fh))) (set! author (str (fmpg_file_author fh))) (set! album (str (fmpg_file_album fh))) (set! genre (str (fmpg_file_genre fh))) (set! comment (str (fmpg_file_comment fh))) (set! copyright (str (fmpg_file_copyright fh))) (set! year (fmpg_file_year fh)) (set! track (fmpg_file_track fh)) (set! bitrate (fmpg_file_bitrate fh))) (define (init file) (unless (ok? (fmpg_open_file fh file)) (error (format "fmpg_open_file: could not open ~a" file))) (set! ffmpeg-file (format "~a" file)) (set! current-pcm-pos 0) (fetch-info) #t) (define (ffmpeg-format cb) (cb current-pcm-pos rate channels sample-bits sample-bytes pcm-length)) (define (info) (info-sound "file : ~a" ffmpeg-file) (info-sound "audio-streams : ~a" audio-streams) (info-sound "channels : ~a" channels) (info-sound "sample-bits : ~a" sample-bits) (info-sound "sample-bytes : ~a" sample-bytes) (info-sound "rate : ~a" rate) (info-sound "pcm-length : ~a" pcm-length) (info-sound "duration-ms : ~a" duration-ms) (info-sound "title : ~a" title) (info-sound "author : ~a" author) (info-sound "album : ~a" album) (info-sound "genre : ~a" genre) (info-sound "year : ~a" year) (info-sound "track : ~a" track) (info-sound "bitrate : ~a" bitrate) #t) (define (close) (unless (eq? fh #f) (when (ok? (fmpg_is_open fh)) (fmpg_close fh)) (set! channels -1) (set! pcm-length -1) (set! duration-ms -1) (set! rate -1) (set! sample-bits -1) (set! sample-bytes -1) (set! audio-streams -1) (set! ffmpeg-file "") #t)) (define (read cb format-cb) ;; Unlike mpg123, this shim already has a fixed output format after ;; fmpg_open_file. Still report the format lazily on the first read so ;; the decoder layer can keep exactly the same structure as mp3-decoder. (when (= current-pcm-pos 0) (ffmpeg-format format-cb)) (if (ok? (fmpg_decode_next fh)) (let-values ([(buffer size) (copy-current-buffer fh)]) (cond ((or (eq? buffer #f) (<= size 0)) ;; Defensive: fmpg_decode_next should only return 1 when there ;; is PCM data, but if the native side ever returns an empty ;; block, simply read again. (read cb format-cb)) (else ;; The start sample is the absolute music position of the first ;; sample frame in this buffer. This is more useful than the ;; end position for UI and progress reporting. (let ((pcm-pos (fmpg_buffer_start_sample fh))) (set! current-pcm-pos (fmpg_buffer_end_sample fh)) (cb 'data pcm-pos buffer size))))) (cb 'done -1 #f 0)) #t) (define (seek pcm-pos) (let* ((r (if (and (integer? rate) (> rate 0)) rate 44100)) (ms (inexact->exact (round (* 1000.0 (/ pcm-pos r)))))) (unless (ok? (fmpg_seek_ms fh ms)) (error (format "fmpg_seek_ms: could not seek to sample ~a (~a ms)" pcm-pos ms))) (set! current-pcm-pos (fmpg_sample_position fh)) #t)) (define (tell) (if (eq? fh #f) 0 (fmpg_sample_position fh))) (define (metadata) (let ((h (make-hash))) (hash-set! h 'title title) (hash-set! h 'author author) (hash-set! h 'album album) (hash-set! h 'genre genre) (hash-set! h 'comment comment) (hash-set! h 'copyright copyright) (hash-set! h 'year year) (hash-set! h 'track track) (hash-set! h 'bitrate bitrate) (hash-set! h 'duration-ms duration-ms) (hash-set! h 'audio-streams audio-streams) h)) (define (version) (fmpg-version)) (lambda (cmd . args) (cond [(eq? cmd 'new) (new)] [(eq? cmd 'delete) (delete)] [(eq? cmd 'init) (init (car args))] [(eq? cmd 'close) (close)] [(eq? cmd 'format) (ffmpeg-format (car args))] [(eq? cmd 'info) (info)] [(eq? cmd 'read) (read (car args) (cadr args))] [(eq? cmd 'seek) (seek (car args))] [(eq? cmd 'tell) (tell)] [(eq? cmd 'file) ffmpeg-file] [(eq? cmd 'metadata) (metadata)] [(eq? cmd 'version) (version)] [else (error (format "Unknown command: ~a" cmd))]))) ); end of module