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