diff --git a/cl-streamer/aac-encoder.lisp b/cl-streamer/aac-encoder.lisp new file mode 100644 index 0000000..94d1e15 --- /dev/null +++ b/cl-streamer/aac-encoder.lisp @@ -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))))))))) diff --git a/cl-streamer/buffer.lisp b/cl-streamer/buffer.lisp index 954eeeb..e8abee8 100644 --- a/cl-streamer/buffer.lisp +++ b/cl-streamer/buffer.lisp @@ -42,8 +42,8 @@ for j = write-pos then (mod (1+ j) size) do (setf (aref buf-data j) (aref data i)) finally (setf (buffer-write-pos buffer) (mod (1+ j) size)))) - (bt:condition-notify (buffer-not-empty buffer)))) - len) + (bt:condition-notify (buffer-not-empty buffer))) + len)) (defun buffer-read (buffer output &key (start 0) (end (length output)) (blocking t)) "Read bytes from BUFFER into OUTPUT. Returns number of bytes read. diff --git a/cl-streamer/cl-streamer.asd b/cl-streamer/cl-streamer.asd index d00321b..9ed8158 100644 --- a/cl-streamer/cl-streamer.asd +++ b/cl-streamer/cl-streamer.asd @@ -24,7 +24,8 @@ :depends-on (#:cl-streamer #:harmony #:cl-mixed - #:cl-mixed-mpg123) + #:cl-mixed-mpg123 + #:cl-mixed-flac) :components ((:file "harmony-backend"))) (asdf:defsystem #:cl-streamer/encoder @@ -33,3 +34,10 @@ #:cffi) :components ((:file "lame-ffi") (: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"))) diff --git a/cl-streamer/encoder.lisp b/cl-streamer/encoder.lisp new file mode 100644 index 0000000..b6870db --- /dev/null +++ b/cl-streamer/encoder.lisp @@ -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)) diff --git a/cl-streamer/fdkaac-ffi.lisp b/cl-streamer/fdkaac-ffi.lisp new file mode 100644 index 0000000..e522f28 --- /dev/null +++ b/cl-streamer/fdkaac-ffi.lisp @@ -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)) diff --git a/cl-streamer/icy-protocol.lisp b/cl-streamer/icy-protocol.lisp index da872d8..b5e9f2f 100644 --- a/cl-streamer/icy-protocol.lisp +++ b/cl-streamer/icy-protocol.lisp @@ -32,7 +32,6 @@ "Parse an ICY/HTTP request. Returns (values mount-point wants-metadata-p). HEADERS is an alist of (name . value) pairs." (let* ((parts (split-sequence:split-sequence #\Space request-line)) - (method (first parts)) (path (second parts)) (icy-metadata-header (cdr (assoc "icy-metadata" headers :test #'string-equal)))) (values path diff --git a/cl-streamer/lame-ffi.lisp b/cl-streamer/lame-ffi.lisp new file mode 100644 index 0000000..2ff4409 --- /dev/null +++ b/cl-streamer/lame-ffi.lisp @@ -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)