Work-in-progress repo for ambisonics extensions for OM-SoX
Marlon Schumacher
4 days ago 3c36efb7ea5dd8cccd9bcc47f4f6e71ee4cfcbb2
commit | author | age
b36c86 1 ;Authors: 
MS 2 ; A. Nguyen, 2025
3 ; M. Schumacher, 2025
791513 4
AN 5 ; Design limitations:
6 ; 1) Sounds must have identical sample rate; otherwise, SoX fails silently, literally.
7
8 ; Questions:
197ce0 9 ; 1) Is there a built-in (flatten lst)-function? Yes, om:flat
MS 10 ; 2) Implement up to which order? <=> CLI issues to be expected (e.g., maximum command character length)? max character length for CLI should be changeable 
791513 11
AN 12 (in-package :om)
13
14 ;;; SOX-HOAENCODE ========================
15
16 ; Util
17 (defun flatten (structure)
18     (cond 
19         ((null structure) 
20             nil)
21         ((atom structure) 
22             (list structure))
23         (t 
24             (mapcan #'flatten structure))
25     )
26 )
27
28 (defun sox-hoaencode-deg-to-rad (x)
29     (* (/ x 180) pi)
30 )
31
32 (defun sox-hoaencode-double-to-float (lst)
33     (mapcar 
34         (lambda (x) (float x 0.0S0)) 
35         lst
36     )
37 )
38
197ce0 39 ;  ### Ambisonics ###
MS 40
e43e60 41 (defun sox-hoaencode-sn3d-factor (order degree)
MS 42 "This is a placeholder for documentation"
791513 43     (ecase order
AN 44         (0 1)
45         (1 1)
46         (2 
47             (ecase (abs degree)
48                 (2 (/ (sqrt 3) 6))
49                 (1 (/ (sqrt 3) 3))
50                 (0 1)
51             ))
52         (3 
53             (ecase (abs degree)
54                 (3 (/ (sqrt 10) 60))
55                 (2 (/ (sqrt 15) 30))
56                 (1 (/ (sqrt 6) 6))
57                 (0 1)
58             )
59         )
60     )
61 )
62
63 (defun sox-hoaencode-azimuth-factor (degree theta_deg)
e43e60 64 "This is a placeholder for documentation"
791513 65     (let* 
AN 66         (
67             (theta (sox-hoaencode-deg-to-rad theta_deg))
68         )
69         (if (< degree 0) 
70             (sin (* (abs degree) theta)) 
71             (cos (* (abs degree) theta))
72         )
73     )
74 )
75
76 (defun sox-hoaencode-elevation-factor (order degree phi_deg)
e43e60 77 "This is a placeholder for documentation"
791513 78     (let* 
AN 79         (
80             (phi (sox-hoaencode-deg-to-rad phi_deg))
81         )
82         (ecase order
83             (0 1)
84             (1 
85                 (ecase (abs degree)
86                     (0 (sin phi))
87                     (1 (cos phi))
88                 ))
89             (2 
90                 (ecase (abs degree)
91                     (0 (- (/ (* 3 (expt (sin phi) 2)) 2) (/ 1 2)))
92                     (1 (/ (* 3 (expt (sin phi) 2)) 2))
93                     (2 (* 3 (expt (cos phi) 2)))
94                 ))
95             (3 
96                 (ecase (abs degree)
97                     (0 (/ (* (sin phi) (- (* 5 (expt (sin phi) 2)) 3)) 2))
98                     (1 (- (* 6 (cos phi)) (/ (* 15 (expt (cos phi) 3)) 2)))
99                     (2 (* -15 (sin phi) (- (expt (sin phi) 2) 1)))
100                     (3 (* 15 (expt (cos phi) 3)))
101                 ))
102         )
103     ))
104
197ce0 105 ; #### Utility functions ####
791513 106
e43e60 107 #|
791513 108 (defun sox-hoaencode-auto-convert-positions (positions)
197ce0 109 "Ensures that positions are given as '((azimuth elevation) ...) (in degrees), i.e.
MS 110 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).
111 2) if list of list of two values, input is assumed to be '(azimuth elevation) coordinates and won't be transformed any further.
112 3) if 3dc, input is transformed to '(azimuth elevation) coordinates.
113 "
791513 114     (cond 
AN 115         ((subtypep (type-of positions) '3dc)  
116             (progn ; convert xyz->aed, keep azimuth and elevation only.
117                 (mat-trans 
118                     (butlast 
119                         (mat-trans 
120                             (mapcar 
121                                 (lambda (xyz) (multiple-value-list (om:xyz->aed (first xyz) (second xyz) (third xyz)))) 
122                                 (mat-trans (list (x-points positions) (y-points positions) (z-points positions)))
123                             ))))))
124         ((subtypep (type-of positions) 'list) 
125             (cond
126                 ((= 2 (length (first positions)))
127                     positions)
128                 ((= 3 (length (first positions))) 
129                     (mat-trans 
130                         (butlast 
131                             (mat-trans 
132                                 (mapcar 
133                                     (lambda (xyz) (multiple-value-list (om:xyz->aed (first xyz) (second xyz) (third xyz)))) 
134                                     positions
135                                 )))))
a590ad 136                 (t (error "Positions must consist of lists of length 2 (ae) or 3 (xyz)."))
791513 137             ))
a590ad 138         (t (error "Positions must be of type 3dc or list."))))
e43e60 139 |#
197ce0 140
MS 141
142 ; ####### High-level API ########## 
143
144
791513 145 (defun sox-hoaencode-gain-single-component (order degree azimuth_deg elevation_deg)
197ce0 146   "Returns the gain value (linear, -1 to 1) for a single ACN-channel"
791513 147     (let
AN 148         (
149             ; It is assumed that azimuth_deg follows the implementation details of SpatDIF, 
150             ; where azimuth_deg runs counterclockwise. 
151             ; However, the formulas inside assume that azimuth_deg runs clockwise, i.e. 
152             ; the sign of azimuth_deg must be inverted.
153             (azimuth_deg_inverted (* -1 azimuth_deg))
154         )
155         (* 
156             (sox-hoaencode-sn3d-factor order degree) 
157             (sox-hoaencode-azimuth-factor degree azimuth_deg_inverted) 
158             (sox-hoaencode-elevation-factor order degree elevation_deg)))
159     )
160
197ce0 161 ; (sox-hoaencode-gain-single-component 1 1 45 0)
MS 162 ; what is the difference between "order" and "degree"?
163
164
791513 165 (defun sox-hoaencode-gains-by-order (order azimuth_deg elevation_deg)
197ce0 166   "Returns the gain values for all components at a specific order"
791513 167     (loop for degree from (* -1 order) to order collect 
AN 168         (sox-hoaencode-gain-single-component order degree azimuth_deg elevation_deg)))
169
197ce0 170
791513 171 (defun sox-hoaencode-gains-up-to-order (order azimuth_deg elevation_deg)
197ce0 172   "Returns the gain values for all components up to a specific order"
791513 173     (sox-hoaencode-double-to-float 
AN 174         (flatten
175             (loop for ord from 0 to order collect 
176                 (sox-hoaencode-gains-by-order ord azimuth_deg elevation_deg)))))
177
178
179 (defclass! sox-hoaencode (sox-input)
180     (
e43e60 181      (gain :accessor gain :initarg :gain :initform 0 :documentation "gain value in dB (decibel)")
MS 182      (azimuth :accessor azimuth :initarg :azimuth :initform 0 :documentation "azimuth angle in degrees")
183      (elevation :accessor elevation :initarg :elevation :initform 0 :documentation "elevation angle in degrees")
184      (order :accessor order :initarg :order :initform 3 :documentation *sox-hoaencode-order-doc*)
791513 185     )
AN 186     (:icon 100)
124c93 187     (:documentation "Sox-hoaencode encodes <sound> into a <order>-th ambisonic (HOA) signal at <positions>.
791513 188
AN 189     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, ...).")
190 )
191
192 (defmethod initialize-instance :after ((self sox-hoaencode) &rest l)
193     (declare (ignore l))
194     (when (sound self)
b36c86 195         (sox-init-sound self 'atom)
791513 196     )
AN 197 )
197ce0 198
MS 199