feat: Add MP3 and AAC encoder FFI bindings

- LAME FFI (lame-ffi.lisp) for MP3 encoding
- FDK-AAC FFI (fdkaac-ffi.lisp) for AAC encoding
- High-level encoder wrappers (encoder.lisp, aac-encoder.lisp)
- Fix compilation warnings in buffer.lisp and icy-protocol.lisp

Both encoders tested and loading successfully:
- LAME version: 3.101
- FDK-AAC: libfdk-aac.so.2

Next steps: Harmony integration for audio pipeline
This commit is contained in:
Glenn Thompson 2026-03-03 16:46:29 +03:00
parent e1be88a54a
commit 6c15441a08
7 changed files with 462 additions and 4 deletions

View File

@ -0,0 +1,128 @@
(in-package #:cl-streamer)
(defclass aac-encoder ()
((handle :initform nil :accessor encoder-handle)
(sample-rate :initarg :sample-rate :accessor aac-encoder-sample-rate :initform 44100)
(channels :initarg :channels :accessor aac-encoder-channels :initform 2)
(bitrate :initarg :bitrate :accessor aac-encoder-bitrate :initform 128000)
(aot :initarg :aot :accessor aac-encoder-aot :initform :aot-aac-lc)
(out-buffer :initform nil :accessor aac-encoder-out-buffer)
(out-buffer-size :initform (* 1024 8) :accessor aac-encoder-out-buffer-size)
(frame-length :initform 1024 :accessor aac-encoder-frame-length)))
(defun make-aac-encoder (&key (sample-rate 44100) (channels 2) (bitrate 128000))
"Create an AAC encoder with the specified parameters.
BITRATE is in bits per second (e.g., 128000 for 128kbps)."
(let ((encoder (make-instance 'aac-encoder
:sample-rate sample-rate
:channels channels
:bitrate bitrate)))
(initialize-aac-encoder encoder)
encoder))
(defun initialize-aac-encoder (encoder)
"Initialize the FDK-AAC encoder with current settings."
(cffi:with-foreign-object (handle-ptr :pointer)
(let ((result (aac-enc-open handle-ptr 0 (aac-encoder-channels encoder))))
(unless (zerop result)
(error 'encoding-error :format :aac
:message (format nil "aacEncOpen failed: ~A" result)))
(setf (encoder-handle encoder) (cffi:mem-ref handle-ptr :pointer))))
(let ((handle (encoder-handle encoder)))
(aac-encoder-set-param handle :aacenc-aot 2)
(aac-encoder-set-param handle :aacenc-samplerate (aac-encoder-sample-rate encoder))
(aac-encoder-set-param handle :aacenc-channelmode
(if (= (aac-encoder-channels encoder) 1) 1 2))
(aac-encoder-set-param handle :aacenc-channelorder 1)
(aac-encoder-set-param handle :aacenc-bitrate (aac-encoder-bitrate encoder))
(aac-encoder-set-param handle :aacenc-transmux 2)
(aac-encoder-set-param handle :aacenc-afterburner 1)
(cffi:with-foreign-object (in-args '(:struct aacenc-in-args))
(cffi:with-foreign-object (out-args '(:struct aacenc-out-args))
(setf (cffi:foreign-slot-value in-args '(:struct aacenc-in-args) 'num-in-samples) -1)
(setf (cffi:foreign-slot-value in-args '(:struct aacenc-in-args) 'num-ancillary-bytes) 0)
(let ((result (aac-enc-encode handle (cffi:null-pointer) (cffi:null-pointer)
in-args out-args)))
(unless (zerop result)
(aac-enc-close (cffi:foreign-alloc :pointer :initial-element handle))
(error 'encoding-error :format :aac
:message (format nil "aacEncEncode init failed: ~A" result))))))
(cffi:with-foreign-object (info '(:struct aacenc-info-struct))
(aac-enc-info handle info)
(setf (aac-encoder-frame-length encoder)
(cffi:foreign-slot-value info '(:struct aacenc-info-struct) 'frame-length))
(setf (aac-encoder-out-buffer-size encoder)
(cffi:foreign-slot-value info '(:struct aacenc-info-struct) 'max-out-buf-bytes)))
(setf (aac-encoder-out-buffer encoder)
(cffi:foreign-alloc :unsigned-char :count (aac-encoder-out-buffer-size encoder)))
(log:info "AAC encoder initialized: ~Akbps, ~AHz, ~A channels, frame-length=~A"
(floor (aac-encoder-bitrate encoder) 1000)
(aac-encoder-sample-rate encoder)
(aac-encoder-channels encoder)
(aac-encoder-frame-length encoder))
encoder))
(defun close-aac-encoder (encoder)
"Close the AAC encoder and free resources."
(when (encoder-handle encoder)
(cffi:with-foreign-object (handle-ptr :pointer)
(setf (cffi:mem-ref handle-ptr :pointer) (encoder-handle encoder))
(aac-enc-close handle-ptr))
(setf (encoder-handle encoder) nil))
(when (aac-encoder-out-buffer encoder)
(cffi:foreign-free (aac-encoder-out-buffer encoder))
(setf (aac-encoder-out-buffer encoder) nil)))
(defun encode-aac-pcm (encoder pcm-samples num-samples)
"Encode PCM samples (16-bit signed interleaved) to AAC.
Returns a byte vector of AAC data (ADTS frames)."
(let* ((handle (encoder-handle encoder))
(channels (aac-encoder-channels encoder))
(out-buf (aac-encoder-out-buffer encoder))
(out-buf-size (aac-encoder-out-buffer-size encoder)))
(cffi:with-pointer-to-vector-data (pcm-ptr pcm-samples)
(cffi:with-foreign-objects ((in-buf-desc '(:struct aacenc-buf-desc))
(out-buf-desc '(:struct aacenc-buf-desc))
(in-args '(:struct aacenc-in-args))
(out-args '(:struct aacenc-out-args))
(in-buf-ptr :pointer)
(in-buf-id :int)
(in-buf-size :int)
(in-buf-el-size :int)
(out-buf-ptr :pointer)
(out-buf-id :int)
(out-buf-size-ptr :int)
(out-buf-el-size :int))
(setf (cffi:mem-ref in-buf-ptr :pointer) pcm-ptr)
(setf (cffi:mem-ref in-buf-id :int) 0)
(setf (cffi:mem-ref in-buf-size :int) (* num-samples channels 2))
(setf (cffi:mem-ref in-buf-el-size :int) 2)
(setf (cffi:foreign-slot-value in-buf-desc '(:struct aacenc-buf-desc) 'num-bufs) 1)
(setf (cffi:foreign-slot-value in-buf-desc '(:struct aacenc-buf-desc) 'bufs) in-buf-ptr)
(setf (cffi:foreign-slot-value in-buf-desc '(:struct aacenc-buf-desc) 'buf-ids) in-buf-id)
(setf (cffi:foreign-slot-value in-buf-desc '(:struct aacenc-buf-desc) 'buf-sizes) in-buf-size)
(setf (cffi:foreign-slot-value in-buf-desc '(:struct aacenc-buf-desc) 'buf-el-sizes) in-buf-el-size)
(setf (cffi:mem-ref out-buf-ptr :pointer) out-buf)
(setf (cffi:mem-ref out-buf-id :int) 1)
(setf (cffi:mem-ref out-buf-size-ptr :int) out-buf-size)
(setf (cffi:mem-ref out-buf-el-size :int) 1)
(setf (cffi:foreign-slot-value out-buf-desc '(:struct aacenc-buf-desc) 'num-bufs) 1)
(setf (cffi:foreign-slot-value out-buf-desc '(:struct aacenc-buf-desc) 'bufs) out-buf-ptr)
(setf (cffi:foreign-slot-value out-buf-desc '(:struct aacenc-buf-desc) 'buf-ids) out-buf-id)
(setf (cffi:foreign-slot-value out-buf-desc '(:struct aacenc-buf-desc) 'buf-sizes) out-buf-size-ptr)
(setf (cffi:foreign-slot-value out-buf-desc '(:struct aacenc-buf-desc) 'buf-el-sizes) out-buf-el-size)
(setf (cffi:foreign-slot-value in-args '(:struct aacenc-in-args) 'num-in-samples)
(* num-samples channels))
(setf (cffi:foreign-slot-value in-args '(:struct aacenc-in-args) 'num-ancillary-bytes) 0)
(let ((result (aac-enc-encode handle in-buf-desc out-buf-desc in-args out-args)))
(unless (zerop result)
(error 'encoding-error :format :aac
:message (format nil "aacEncEncode failed: ~A" result)))
(let ((bytes-written (cffi:foreign-slot-value out-args '(:struct aacenc-out-args)
'num-out-bytes)))
(if (> bytes-written 0)
(let ((result-vec (make-array bytes-written :element-type '(unsigned-byte 8))))
(loop for i below bytes-written
do (setf (aref result-vec i) (cffi:mem-aref out-buf :unsigned-char i)))
result-vec)
(make-array 0 :element-type '(unsigned-byte 8)))))))))

View File

@ -42,8 +42,8 @@
for j = write-pos then (mod (1+ j) size) for j = write-pos then (mod (1+ j) size)
do (setf (aref buf-data j) (aref data i)) do (setf (aref buf-data j) (aref data i))
finally (setf (buffer-write-pos buffer) (mod (1+ j) size)))) finally (setf (buffer-write-pos buffer) (mod (1+ j) size))))
(bt:condition-notify (buffer-not-empty buffer)))) (bt:condition-notify (buffer-not-empty buffer)))
len) len))
(defun buffer-read (buffer output &key (start 0) (end (length output)) (blocking t)) (defun buffer-read (buffer output &key (start 0) (end (length output)) (blocking t))
"Read bytes from BUFFER into OUTPUT. Returns number of bytes read. "Read bytes from BUFFER into OUTPUT. Returns number of bytes read.

View File

@ -24,7 +24,8 @@
:depends-on (#:cl-streamer :depends-on (#:cl-streamer
#:harmony #:harmony
#:cl-mixed #:cl-mixed
#:cl-mixed-mpg123) #:cl-mixed-mpg123
#:cl-mixed-flac)
:components ((:file "harmony-backend"))) :components ((:file "harmony-backend")))
(asdf:defsystem #:cl-streamer/encoder (asdf:defsystem #:cl-streamer/encoder
@ -33,3 +34,10 @@
#:cffi) #:cffi)
:components ((:file "lame-ffi") :components ((:file "lame-ffi")
(:file "encoder"))) (:file "encoder")))
(asdf:defsystem #:cl-streamer/aac-encoder
:description "AAC encoding for cl-streamer (FDK-AAC)"
:depends-on (#:cl-streamer
#:cffi)
:components ((:file "fdkaac-ffi")
(:file "aac-encoder")))

96
cl-streamer/encoder.lisp Normal file
View File

@ -0,0 +1,96 @@
(in-package #:cl-streamer)
(defclass mp3-encoder ()
((lame :initform nil :accessor encoder-lame)
(sample-rate :initarg :sample-rate :accessor encoder-sample-rate :initform 44100)
(channels :initarg :channels :accessor encoder-channels :initform 2)
(bitrate :initarg :bitrate :accessor encoder-bitrate :initform 128)
(quality :initarg :quality :accessor encoder-quality :initform 5)
(mp3-buffer :initform nil :accessor encoder-mp3-buffer)
(mp3-buffer-size :initform (* 1024 8) :accessor encoder-mp3-buffer-size)))
(defun make-mp3-encoder (&key (sample-rate 44100) (channels 2) (bitrate 128) (quality 5))
"Create an MP3 encoder with the specified parameters.
QUALITY: 0=best/slowest, 9=worst/fastest. 5 is good default."
(let ((encoder (make-instance 'mp3-encoder
:sample-rate sample-rate
:channels channels
:bitrate bitrate
:quality quality)))
(initialize-encoder encoder)
encoder))
(defun initialize-encoder (encoder)
"Initialize the LAME encoder with current settings."
(let ((lame (lame-init)))
(when (cffi:null-pointer-p lame)
(error 'encoding-error :format :mp3 :message "Failed to initialize LAME"))
(setf (encoder-lame encoder) lame)
(lame-set-in-samplerate lame (encoder-sample-rate encoder))
(lame-set-out-samplerate lame (encoder-sample-rate encoder))
(lame-set-num-channels lame (encoder-channels encoder))
(lame-set-mode lame (if (= (encoder-channels encoder) 1) :mono :joint-stereo))
(lame-set-brate lame (encoder-bitrate encoder))
(lame-set-quality lame (encoder-quality encoder))
(lame-set-vbr lame :vbr-off)
(let ((result (lame-init-params lame)))
(when (< result 0)
(lame-close lame)
(error 'encoding-error :format :mp3
:message (format nil "LAME init-params failed: ~A" result))))
(setf (encoder-mp3-buffer encoder)
(cffi:foreign-alloc :unsigned-char :count (encoder-mp3-buffer-size encoder)))
(log:info "MP3 encoder initialized: ~Akbps, ~AHz, ~A channels"
(encoder-bitrate encoder)
(encoder-sample-rate encoder)
(encoder-channels encoder))
encoder))
(defun close-encoder (encoder)
"Close the encoder and free resources."
(when (encoder-lame encoder)
(lame-close (encoder-lame encoder))
(setf (encoder-lame encoder) nil))
(when (encoder-mp3-buffer encoder)
(cffi:foreign-free (encoder-mp3-buffer encoder))
(setf (encoder-mp3-buffer encoder) nil)))
(defun encode-pcm-interleaved (encoder pcm-samples num-samples)
"Encode interleaved PCM samples (16-bit signed) to MP3.
PCM-SAMPLES should be a (simple-array (signed-byte 16) (*)).
Returns a byte vector of MP3 data."
(let* ((lame (encoder-lame encoder))
(mp3-buf (encoder-mp3-buffer encoder))
(mp3-buf-size (encoder-mp3-buffer-size encoder)))
(cffi:with-pointer-to-vector-data (pcm-ptr pcm-samples)
(let ((bytes-written (lame-encode-buffer-interleaved
lame pcm-ptr num-samples mp3-buf mp3-buf-size)))
(cond
((< bytes-written 0)
(error 'encoding-error :format :mp3
:message (format nil "Encode failed: ~A" bytes-written)))
((= bytes-written 0)
(make-array 0 :element-type '(unsigned-byte 8)))
(t
(let ((result (make-array bytes-written :element-type '(unsigned-byte 8))))
(loop for i below bytes-written
do (setf (aref result i) (cffi:mem-aref mp3-buf :unsigned-char i)))
result)))))))
(defun encode-flush (encoder)
"Flush any remaining MP3 data from the encoder.
Call this when done encoding to get final frames."
(let* ((lame (encoder-lame encoder))
(mp3-buf (encoder-mp3-buffer encoder))
(mp3-buf-size (encoder-mp3-buffer-size encoder)))
(let ((bytes-written (lame-encode-flush lame mp3-buf mp3-buf-size)))
(if (> bytes-written 0)
(let ((result (make-array bytes-written :element-type '(unsigned-byte 8))))
(loop for i below bytes-written
do (setf (aref result i) (cffi:mem-aref mp3-buf :unsigned-char i)))
result)
(make-array 0 :element-type '(unsigned-byte 8))))))
(defun lame-version ()
"Return the LAME library version string."
(get-lame-version))

135
cl-streamer/fdkaac-ffi.lisp Normal file
View File

@ -0,0 +1,135 @@
(in-package #:cl-streamer)
(cffi:define-foreign-library libfdkaac
(:unix (:or "libfdk-aac.so.2" "libfdk-aac.so"))
(:darwin "libfdk-aac.dylib")
(:windows "libfdk-aac.dll")
(t (:default "libfdk-aac")))
(cffi:use-foreign-library libfdkaac)
(cffi:defctype aac-encoder-handle :pointer)
(cffi:defcenum aac-encoder-param
(:aacenc-aot #x0100)
(:aacenc-bitrate #x0101)
(:aacenc-bitratemode #x0102)
(:aacenc-samplerate #x0103)
(:aacenc-sbr-mode #x0104)
(:aacenc-granule-length #x0105)
(:aacenc-channelmode #x0106)
(:aacenc-channelorder #x0107)
(:aacenc-sbr-ratio #x0108)
(:aacenc-afterburner #x0200)
(:aacenc-bandwidth #x0203)
(:aacenc-transmux #x0300)
(:aacenc-header-period #x0301)
(:aacenc-signaling-mode #x0302)
(:aacenc-tpsubframes #x0303)
(:aacenc-protection #x0306)
(:aacenc-ancillary-bitrate #x0500)
(:aacenc-metadata-mode #x0600))
(cffi:defcenum aac-encoder-error
(:aacenc-ok #x0000)
(:aacenc-invalid-handle #x0020)
(:aacenc-memory-error #x0021)
(:aacenc-unsupported-parameter #x0022)
(:aacenc-invalid-config #x0023)
(:aacenc-init-error #x0040)
(:aacenc-init-aac-error #x0041)
(:aacenc-init-sbr-error #x0042)
(:aacenc-init-tp-error #x0043)
(:aacenc-init-meta-error #x0044)
(:aacenc-encode-error #x0060)
(:aacenc-encode-eof #x0080))
(cffi:defcenum aac-channel-mode
(:mode-invalid -1)
(:mode-unknown 0)
(:mode-1 1)
(:mode-2 2)
(:mode-1-2 3)
(:mode-1-2-1 4)
(:mode-1-2-2 5)
(:mode-1-2-2-1 6)
(:mode-1-2-2-2-1 7))
(cffi:defcenum aac-transmux
(:tt-unknown -1)
(:tt-raw 0)
(:tt-adif 1)
(:tt-adts 2)
(:tt-latm-mcp1 6)
(:tt-latm-mcp0 7)
(:tt-loas 10))
(cffi:defcenum aac-aot
(:aot-none -1)
(:aot-null 0)
(:aot-aac-main 1)
(:aot-aac-lc 2)
(:aot-aac-ssr 3)
(:aot-aac-ltp 4)
(:aot-sbr 5)
(:aot-aac-scal 6)
(:aot-er-aac-lc 17)
(:aot-er-aac-ld 23)
(:aot-er-aac-eld 39)
(:aot-ps 29)
(:aot-mp2-aac-lc 129)
(:aot-mp2-sbr 132))
(cffi:defcstruct aacenc-buf-desc
(num-bufs :int)
(bufs :pointer)
(buf-ids :pointer)
(buf-sizes :pointer)
(buf-el-sizes :pointer))
(cffi:defcstruct aacenc-in-args
(num-in-samples :int)
(num-ancillary-bytes :int))
(cffi:defcstruct aacenc-out-args
(num-out-bytes :int)
(num-in-samples :int)
(num-ancillary-bytes :int))
(cffi:defcstruct aacenc-info-struct
(max-out-buf-bytes :uint)
(max-ancillary-bytes :uint)
(in-buf-fill-level :uint)
(input-channels :uint)
(frame-length :uint)
(encoder-delay :uint)
(conf-buf :pointer)
(conf-size :uint))
(cffi:defcfun ("aacEncOpen" aac-enc-open) :int
(ph-aac-encoder :pointer)
(enc-modules :uint)
(max-channels :uint))
(cffi:defcfun ("aacEncClose" aac-enc-close) :int
(ph-aac-encoder :pointer))
(cffi:defcfun ("aacEncEncode" aac-enc-encode) :int
(h-aac-encoder aac-encoder-handle)
(in-buf-desc :pointer)
(out-buf-desc :pointer)
(in-args :pointer)
(out-args :pointer))
(cffi:defcfun ("aacEncInfo" aac-enc-info) :int
(h-aac-encoder aac-encoder-handle)
(p-info :pointer))
(cffi:defcfun ("aacEncoder_SetParam" aac-encoder-set-param) :int
(h-aac-encoder aac-encoder-handle)
(param aac-encoder-param)
(value :uint))
(cffi:defcfun ("aacEncoder_GetParam" aac-encoder-get-param) :uint
(h-aac-encoder aac-encoder-handle)
(param aac-encoder-param))

View File

@ -32,7 +32,6 @@
"Parse an ICY/HTTP request. Returns (values mount-point wants-metadata-p). "Parse an ICY/HTTP request. Returns (values mount-point wants-metadata-p).
HEADERS is an alist of (name . value) pairs." HEADERS is an alist of (name . value) pairs."
(let* ((parts (split-sequence:split-sequence #\Space request-line)) (let* ((parts (split-sequence:split-sequence #\Space request-line))
(method (first parts))
(path (second parts)) (path (second parts))
(icy-metadata-header (cdr (assoc "icy-metadata" headers :test #'string-equal)))) (icy-metadata-header (cdr (assoc "icy-metadata" headers :test #'string-equal))))
(values path (values path

92
cl-streamer/lame-ffi.lisp Normal file
View File

@ -0,0 +1,92 @@
(in-package #:cl-streamer)
(cffi:define-foreign-library liblame
(:unix (:or "libmp3lame.so.0" "libmp3lame.so"))
(:darwin "libmp3lame.dylib")
(:windows "libmp3lame.dll")
(t (:default "libmp3lame")))
(cffi:use-foreign-library liblame)
(cffi:defctype lame-global-flags :pointer)
(cffi:defcenum lame-vbr-mode
(:vbr-off 0)
(:vbr-mt 1)
(:vbr-rh 2)
(:vbr-abr 3)
(:vbr-mtrh 4)
(:vbr-default 4))
(cffi:defcenum lame-mode
(:stereo 0)
(:joint-stereo 1)
(:dual-channel 2)
(:mono 3))
(cffi:defcfun ("lame_init" lame-init) lame-global-flags)
(cffi:defcfun ("lame_close" lame-close) :int
(gfp lame-global-flags))
(cffi:defcfun ("lame_set_in_samplerate" lame-set-in-samplerate) :int
(gfp lame-global-flags)
(rate :int))
(cffi:defcfun ("lame_set_out_samplerate" lame-set-out-samplerate) :int
(gfp lame-global-flags)
(rate :int))
(cffi:defcfun ("lame_set_num_channels" lame-set-num-channels) :int
(gfp lame-global-flags)
(channels :int))
(cffi:defcfun ("lame_set_mode" lame-set-mode) :int
(gfp lame-global-flags)
(mode lame-mode))
(cffi:defcfun ("lame_set_quality" lame-set-quality) :int
(gfp lame-global-flags)
(quality :int))
(cffi:defcfun ("lame_set_brate" lame-set-brate) :int
(gfp lame-global-flags)
(brate :int))
(cffi:defcfun ("lame_set_VBR" lame-set-vbr) :int
(gfp lame-global-flags)
(vbr-mode lame-vbr-mode))
(cffi:defcfun ("lame_set_VBR_quality" lame-set-vbr-quality) :int
(gfp lame-global-flags)
(quality :float))
(cffi:defcfun ("lame_init_params" lame-init-params) :int
(gfp lame-global-flags))
(cffi:defcfun ("lame_encode_buffer_interleaved" lame-encode-buffer-interleaved) :int
(gfp lame-global-flags)
(pcm :pointer)
(num-samples :int)
(mp3buf :pointer)
(mp3buf-size :int))
(cffi:defcfun ("lame_encode_buffer" lame-encode-buffer) :int
(gfp lame-global-flags)
(buffer-l :pointer)
(buffer-r :pointer)
(num-samples :int)
(mp3buf :pointer)
(mp3buf-size :int))
(cffi:defcfun ("lame_encode_flush" lame-encode-flush) :int
(gfp lame-global-flags)
(mp3buf :pointer)
(mp3buf-size :int))
(cffi:defcfun ("lame_get_lametag_frame" lame-get-lametag-frame) :size
(gfp lame-global-flags)
(buffer :pointer)
(size :size))
(cffi:defcfun ("get_lame_version" get-lame-version) :string)