123 lines
5.7 KiB
Common Lisp
123 lines
5.7 KiB
Common Lisp
(defpackage #:cl-streamer/harmony
|
|
(:use #:cl #:alexandria)
|
|
(:local-nicknames (#:harmony #:org.shirakumo.fraf.harmony)
|
|
(#:mixed #:org.shirakumo.fraf.mixed))
|
|
(:export #:audio-pipeline
|
|
#:make-audio-pipeline
|
|
#:start-pipeline
|
|
#:stop-pipeline
|
|
#:play-file
|
|
#:pipeline-encoder
|
|
#:pipeline-server
|
|
#:make-streaming-server))
|
|
|
|
(in-package #:cl-streamer/harmony)
|
|
|
|
(defclass audio-pipeline ()
|
|
((harmony-server :initform nil :accessor pipeline-harmony-server)
|
|
(encoder :initarg :encoder :accessor pipeline-encoder)
|
|
(stream-server :initarg :stream-server :accessor pipeline-server)
|
|
(mount-path :initarg :mount-path :accessor pipeline-mount-path :initform "/stream.mp3")
|
|
(sample-rate :initarg :sample-rate :accessor pipeline-sample-rate :initform 44100)
|
|
(channels :initarg :channels :accessor pipeline-channels :initform 2)
|
|
(running :initform nil :accessor pipeline-running-p)
|
|
(encode-thread :initform nil :accessor pipeline-encode-thread)))
|
|
|
|
(defun make-streaming-server (&key (name "CL-Streamer") (samplerate 44100) (latency 0.02))
|
|
"Create a Harmony server configured for streaming (no audio output).
|
|
Uses :dummy drain so audio goes to buffer instead of speakers."
|
|
(mixed:init)
|
|
(harmony:make-simple-server :name name
|
|
:samplerate samplerate
|
|
:latency latency
|
|
:drain :dummy
|
|
:output-channels 2))
|
|
|
|
(defun make-audio-pipeline (&key encoder stream-server (mount-path "/stream.mp3")
|
|
(sample-rate 44100) (channels 2))
|
|
"Create an audio pipeline connecting Harmony to the stream server via an encoder."
|
|
(make-instance 'audio-pipeline
|
|
:encoder encoder
|
|
:stream-server stream-server
|
|
:mount-path mount-path
|
|
:sample-rate sample-rate
|
|
:channels channels))
|
|
|
|
(defun start-pipeline (pipeline)
|
|
"Start the audio pipeline - initializes Harmony and begins encoding."
|
|
(when (pipeline-running-p pipeline)
|
|
(error "Pipeline already running"))
|
|
(let ((server (make-streaming-server :samplerate (pipeline-sample-rate pipeline))))
|
|
(setf (pipeline-harmony-server pipeline) server)
|
|
(mixed:start server))
|
|
(setf (pipeline-running-p pipeline) t)
|
|
(setf (pipeline-encode-thread pipeline)
|
|
(bt:make-thread (lambda () (encode-loop pipeline))
|
|
:name "cl-streamer-encode"))
|
|
(log:info "Audio pipeline started")
|
|
pipeline)
|
|
|
|
(defun stop-pipeline (pipeline)
|
|
"Stop the audio pipeline."
|
|
(setf (pipeline-running-p pipeline) nil)
|
|
(when (pipeline-encode-thread pipeline)
|
|
(ignore-errors (bt:join-thread (pipeline-encode-thread pipeline)))
|
|
(setf (pipeline-encode-thread pipeline) nil))
|
|
(when (pipeline-harmony-server pipeline)
|
|
(mixed:end (pipeline-harmony-server pipeline))
|
|
(setf (pipeline-harmony-server pipeline) nil))
|
|
(log:info "Audio pipeline stopped")
|
|
pipeline)
|
|
|
|
(defun play-file (pipeline file-path &key (mixer :music))
|
|
"Play an audio file through the pipeline.
|
|
The file will be decoded by Harmony and encoded for streaming."
|
|
(let* ((server (pipeline-harmony-server pipeline))
|
|
(harmony:*server* server))
|
|
(let ((voice (harmony:play file-path :mixer mixer)))
|
|
(log:info "Playing: ~A" file-path)
|
|
voice)))
|
|
|
|
(defun get-pack-buffer (pipeline)
|
|
"Get the packer's pack (bip-buffer) from Harmony's output chain."
|
|
(let* ((server (pipeline-harmony-server pipeline))
|
|
(output (harmony:segment :output server))
|
|
(packer (harmony:segment :packer output)))
|
|
(mixed:pack packer)))
|
|
|
|
(defun encode-loop (pipeline)
|
|
"Main encoding loop - reads PCM from Harmony's packer, encodes, writes to stream."
|
|
(let ((encoder (pipeline-encoder pipeline))
|
|
(mount-path (pipeline-mount-path pipeline))
|
|
(channels (pipeline-channels pipeline))
|
|
(frame-size 1152))
|
|
(loop while (pipeline-running-p pipeline)
|
|
do (handler-case
|
|
(let* ((pack (get-pack-buffer pipeline))
|
|
(available (mixed:available-read pack))
|
|
(needed (* frame-size channels 2)))
|
|
(if (>= available needed)
|
|
(multiple-value-bind (data start size)
|
|
(mixed:request-read pack needed)
|
|
(declare (ignore start))
|
|
(when (and data (>= size needed))
|
|
(let* ((samples (floor size (* channels 2)))
|
|
(pcm-buffer (make-array (* samples channels)
|
|
:element-type '(signed-byte 16))))
|
|
(loop for i below (* samples channels)
|
|
do (setf (aref pcm-buffer i)
|
|
(let ((byte-offset (* i 2)))
|
|
(logior (aref data byte-offset)
|
|
(ash (let ((hi (aref data (1+ byte-offset))))
|
|
(if (> hi 127) (- hi 256) hi))
|
|
8)))))
|
|
(mixed:finish-read pack size)
|
|
(let ((mp3-data (cl-streamer::encode-pcm-interleaved
|
|
encoder pcm-buffer samples)))
|
|
(when (> (length mp3-data) 0)
|
|
(cl-streamer::write-audio-data mount-path mp3-data))))))
|
|
(sleep 0.005)))
|
|
(error (e)
|
|
(log:warn "Encode error: ~A" e)
|
|
(sleep 0.1))))))
|