From 79151350650b58232a1db7c2b1359993575544ff Mon Sep 17 00:00:00 2001 From: Alexander Nguyen <alexander.nguyen@stud.hfm-karlsruhe.de> Date: Sat, 25 Jan 2025 19:01:40 +0100 Subject: [PATCH] First implementation --- sources/classes/sox-hoaencode.lisp | 178 +++++++++++++++++++++++++++++++++++ OM-SoX.lisp | 1 sources/sox-process.lisp | 99 +++++++++++++++++++ resources/icon/100.png | 0 sources/classes/sox-input.lisp | 2 5 files changed, 280 insertions(+), 0 deletions(-) diff --git a/OM-SoX.lisp b/OM-SoX.lisp index 48f90a3..01f3d0d 100644 --- a/OM-SoX.lisp +++ b/OM-SoX.lisp @@ -55,6 +55,7 @@ (defparameter *sox-classes* '( "sox-input" "sox-concatenate" + "sox-hoaencode" "sox-merge" "sox-mix-console" "sox-mix" diff --git a/resources/icon/100.png b/resources/icon/100.png new file mode 100644 index 0000000..a9a0fd6 --- /dev/null +++ b/resources/icon/100.png Binary files differ diff --git a/sources/classes/sox-hoaencode.lisp b/sources/classes/sox-hoaencode.lisp new file mode 100644 index 0000000..e7d1bf7 --- /dev/null +++ b/sources/classes/sox-hoaencode.lisp @@ -0,0 +1,178 @@ +;Authors: A. Nguyen, 2025. + +; Design limitations: +; 1) Sounds must have identical sample rate; otherwise, SoX fails silently, literally. + +; Questions: +; 1) Is there a built-in (flatten lst)-function? +; 2) Implement up to which order? <=> CLI issues to be expected (e.g., maximum command character length)? + +(in-package :om) + +;;; SOX-HOAENCODE ======================== + +; Util +(defun flatten (structure) + (cond + ((null structure) + nil) + ((atom structure) + (list structure)) + (t + (mapcan #'flatten structure)) + ) +) + +(defun sox-hoaencode-deg-to-rad (x) + (* (/ x 180) pi) +) + +(defun sox-hoaencode-double-to-float (lst) + (mapcar + (lambda (x) (float x 0.0S0)) + lst + ) +) + +; Ambisonics +(defun sox-hoaencode-sn3d-factor (order degree) + (ecase order + (0 1) + (1 1) + (2 + (ecase (abs degree) + (2 (/ (sqrt 3) 6)) + (1 (/ (sqrt 3) 3)) + (0 1) + )) + (3 + (ecase (abs degree) + (3 (/ (sqrt 10) 60)) + (2 (/ (sqrt 15) 30)) + (1 (/ (sqrt 6) 6)) + (0 1) + ) + ) + ) +) + +(defun sox-hoaencode-azimuth-factor (degree theta_deg) + (let* + ( + (theta (sox-hoaencode-deg-to-rad theta_deg)) + ) + (if (< degree 0) + (sin (* (abs degree) theta)) + (cos (* (abs degree) theta)) + ) + ) +) + +(defun sox-hoaencode-elevation-factor (order degree phi_deg) + (let* + ( + (phi (sox-hoaencode-deg-to-rad phi_deg)) + ) + (ecase order + (0 1) + (1 + (ecase (abs degree) + (0 (sin phi)) + (1 (cos phi)) + )) + (2 + (ecase (abs degree) + (0 (- (/ (* 3 (expt (sin phi) 2)) 2) (/ 1 2))) + (1 (/ (* 3 (expt (sin phi) 2)) 2)) + (2 (* 3 (expt (cos phi) 2))) + )) + (3 + (ecase (abs degree) + (0 (/ (* (sin phi) (- (* 5 (expt (sin phi) 2)) 3)) 2)) + (1 (- (* 6 (cos phi)) (/ (* 15 (expt (cos phi) 3)) 2))) + (2 (* -15 (sin phi) (- (expt (sin phi) 2) 1))) + (3 (* 15 (expt (cos phi) 3))) + )) + ) + )) + +; Convenience functions + +; Ensures that positions are given as '((azimuth elevation) ...) (in degrees), i.e. +; 1) if list of list of three values, input is assumed to be '(x y z) coordinates and will be transformed to '(azimuth elevation) coordinates (in the navigational spherical coordinate system). +; 2) if list of list of two values, input is assumed to be '(azimuth elevation) coordinates and won't be transformed any further. +; 3) if 3dc, input is transformed to '(azimuth elevation) coordinates. +(defun sox-hoaencode-auto-convert-positions (positions) + (cond + ((subtypep (type-of positions) '3dc) + (progn ; convert xyz->aed, keep azimuth and elevation only. + (mat-trans + (butlast + (mat-trans + (mapcar + (lambda (xyz) (multiple-value-list (om:xyz->aed (first xyz) (second xyz) (third xyz)))) + (mat-trans (list (x-points positions) (y-points positions) (z-points positions))) + )))))) + ((subtypep (type-of positions) 'list) + (cond + ((= 2 (length (first positions))) + positions) + ((= 3 (length (first positions))) + (mat-trans + (butlast + (mat-trans + (mapcar + (lambda (xyz) (multiple-value-list (om:xyz->aed (first xyz) (second xyz) (third xyz)))) + positions + ))))) + (t (error "Input must contain lists of length 2 (ae) or 3 (xyz).")) + )) + (t (error "Input must be 3dc or list.")))) + +; High-level API +; Returns the gain value (linear, -1 to 1) for a single ACN-channel +(defun sox-hoaencode-gain-single-component (order degree azimuth_deg elevation_deg) + (let + ( + ; It is assumed that azimuth_deg follows the implementation details of SpatDIF, + ; where azimuth_deg runs counterclockwise. + ; However, the formulas inside assume that azimuth_deg runs clockwise, i.e. + ; the sign of azimuth_deg must be inverted. + (azimuth_deg_inverted (* -1 azimuth_deg)) + ) + (* + (sox-hoaencode-sn3d-factor order degree) + (sox-hoaencode-azimuth-factor degree azimuth_deg_inverted) + (sox-hoaencode-elevation-factor order degree elevation_deg))) + ) + +; Returns the gain values for all components at a specific order +(defun sox-hoaencode-gains-by-order (order azimuth_deg elevation_deg) + (loop for degree from (* -1 order) to order collect + (sox-hoaencode-gain-single-component order degree azimuth_deg elevation_deg))) + +; Returns the gain values for all components up to a specific order +(defun sox-hoaencode-gains-up-to-order (order azimuth_deg elevation_deg) + (sox-hoaencode-double-to-float + (flatten + (loop for ord from 0 to order collect + (sox-hoaencode-gains-by-order ord azimuth_deg elevation_deg))))) + + +(defclass! sox-hoaencode (sox-input) + ( + (positions :accessor positions :initarg :positions :initform nil :documentation *sox-hoaencode-positions-doc*) + (order :accessor order :initarg :order :initform nil :documentation *sox-hoaencode-order-doc*) + ) + (:icon 100) + (:documentation "Sox-hoaencode transforms <sound> into a <order>-th ambisonic (HOA) signal at <positions>. + + The signal follows the ambiX convention, i.e. it uses SN3D normalization and ACN channel ordering. The resulting file has (<order>+1)^2 channels (order 0: 1 channel, order 1: 4 channels, order 2: 9 channels, order 3: 16 channels, ...).") +) + +(defmethod initialize-instance :after ((self sox-hoaencode) &rest l) + (declare (ignore l)) + (when (sound self) + (sox-init-sound self 'list) + ) +) diff --git a/sources/classes/sox-input.lisp b/sources/classes/sox-input.lisp index d14d93a..27e4e9b 100644 --- a/sources/classes/sox-input.lisp +++ b/sources/classes/sox-input.lisp @@ -31,6 +31,8 @@ (defparameter *sox-dur-doc* "Duration of output audio (sec). [float]") (defparameter *sox-input-doc* "Audio input for sox input modules. [sound object, pathname, string/pipe]") (defparameter *sox-gain-doc* "Gain value for audio input before processing (dBFS). [float, list]") +(defparameter *sox-hoaencode-positions-doc* "Positions as 3dc object, or list of azimuth+elevation values, or list of x+y+z coordinates. [3dc, list]") +(defparameter *sox-hoaencode-order-doc* "Ambisonics order. [int]") (defparameter *sox-panning-doc* "Panning value (decimal number) for panning audio between adjacent channels. [float, list]") (defparameter *sox-channel-matrix-doc* "Specify input channels as a list of lists (matrix). Each sublist contains the contributing input channels for an output channel. [list]") ;*sox-in-channels-doc* diff --git a/sources/sox-process.lisp b/sources/sox-process.lisp index 337a9f3..4f54088 100644 --- a/sources/sox-process.lisp +++ b/sources/sox-process.lisp @@ -178,6 +178,105 @@ (sox-not-found)) ) +; === sox-hoaencode ============= + +(defmethod! sox-process ((sox-input sox-hoaencode) (sox-effect string) &key output filetype samplerate bitdepth recursive batch-mode) + (if (probe-file *sox-path*) + (progn + (let* + ( + (sound + (if (> (length (positions sox-input)) (length (list! (sound sox-input)))) + (repeat-n (first (list! (sound sox-input))) (length (positions sox-input))) + (sound sox-input) + ) + ) + (positions-ae + (sox-hoaencode-auto-convert-positions (positions sox-input)) + ) + ) + (when + (and (find-if 'stringp sound) (equal output "pipe")) + (om-message-abort "Pipe output not possible with this type of input.") + ) + (let* + ( + (outfile (create-path nil output filetype)) + (filenames (loop for soundfile in sound collect (namestring soundfile))) + ) + (setf str (format nil " ~s ~a" (namestring *sox-path*) *sox-options*)) + (if (= (length filenames) 1) + ; case: one file + (let* + ( + (filename (first filenames)) + (position (first positions-ae)) + ) + (if (= (order sox-input) 0) + ; case: order = 0 + (setf str (string+ str (format nil " ~s" filename))) + ; case: order > 0 + (progn + (setf str (string+ str " -M")) + (loop for gain in (sox-hoaencode-gains-up-to-order (order sox-input) (first position) (second position)) do + (setf str (string+ str (format nil " -v~d ~s" gain filename)))) + ) + ) + ) + ; case: multiple files + (if (= (order sox-input) 0) + ; case: order = 0 + (progn + (setf str (string+ str " -m")) + (loop for filename in filenames do + (setf str (string+ str (format nil " ~s" filename))) + ) + ) + ; case: order > 0 + (progn + (setf str (string+ str " -m")) + + ; Create components + (setf component_idx 0) + (loop + for filename in filenames + for position in positions-ae do + (progn + (setf str_component (format nil " ~s ~a -M" (namestring *sox-path*) *sox-options*)) + (loop + for gain in (sox-hoaencode-gains-up-to-order (order sox-input) (first position) (second position)) do + (setf str_component (string+ str_component (format nil " -v~d ~s" gain filename))) + ) + (setf outfile_tmp (format nil "~a~a-~d.~a" (directory-namestring outfile) (pathname-name outfile) component_idx (pathname-type outfile))) + (setf str_component (sox-samplebits str_component bitdepth samplerate outfile_tmp)) + (sox-out str_component filename "new file" outfile_tmp nil) + + (setf component_idx (1+ component_idx)) + + ; Add component path to <str> + (setf str (string+ str (format nil " ~s" outfile_tmp))) + ) + ) + ) + ) + ) + (setf str + (cond + ((equal output "realtime") (sox-samplebits (string+ str " -q ") bitdepth samplerate *sox-audio-device*)) + ((equal output "pipe") (sox-samplebits str bitdepth samplerate "-p")) + (t (sox-samplebits str bitdepth samplerate outfile)) + ) + ) + (setf str (string+ str sox-effect)) + (print str) + (sox-out str sox-input output outfile recursive) + ) + ) + ) + (sox-not-found) + ) +) + ; === sox-mix ============= (defmethod! sox-process ((sox-input sox-mix) (sox-effect string) &key output filetype samplerate bitdepth recursive batch-mode) -- Gitblit v1.9.1