Work-in-progress repo for ambisonics extensions for OM-SoX
Alexander Nguyen
25.01.25 79151350650b58232a1db7c2b1359993575544ff
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:
7 ; 1) Is there a built-in (flatten lst)-function?
8 ; 2) Implement up to which order? <=> CLI issues to be expected (e.g., maximum command character length)?
9
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
37 ; Ambisonics
38 (defun sox-hoaencode-sn3d-factor (order degree)
39     (ecase order
40         (0 1)
41         (1 1)
42         (2 
43             (ecase (abs degree)
44                 (2 (/ (sqrt 3) 6))
45                 (1 (/ (sqrt 3) 3))
46                 (0 1)
47             ))
48         (3 
49             (ecase (abs degree)
50                 (3 (/ (sqrt 10) 60))
51                 (2 (/ (sqrt 15) 30))
52                 (1 (/ (sqrt 6) 6))
53                 (0 1)
54             )
55         )
56     )
57 )
58
59 (defun sox-hoaencode-azimuth-factor (degree theta_deg)
60     (let* 
61         (
62             (theta (sox-hoaencode-deg-to-rad theta_deg))
63         )
64         (if (< degree 0) 
65             (sin (* (abs degree) theta)) 
66             (cos (* (abs degree) theta))
67         )
68     )
69 )
70
71 (defun sox-hoaencode-elevation-factor (order degree phi_deg)
72     (let* 
73         (
74             (phi (sox-hoaencode-deg-to-rad phi_deg))
75         )
76         (ecase order
77             (0 1)
78             (1 
79                 (ecase (abs degree)
80                     (0 (sin phi))
81                     (1 (cos phi))
82                 ))
83             (2 
84                 (ecase (abs degree)
85                     (0 (- (/ (* 3 (expt (sin phi) 2)) 2) (/ 1 2)))
86                     (1 (/ (* 3 (expt (sin phi) 2)) 2))
87                     (2 (* 3 (expt (cos phi) 2)))
88                 ))
89             (3 
90                 (ecase (abs degree)
91                     (0 (/ (* (sin phi) (- (* 5 (expt (sin phi) 2)) 3)) 2))
92                     (1 (- (* 6 (cos phi)) (/ (* 15 (expt (cos phi) 3)) 2)))
93                     (2 (* -15 (sin phi) (- (expt (sin phi) 2) 1)))
94                     (3 (* 15 (expt (cos phi) 3)))
95                 ))
96         )
97     ))
98
99 ; Convenience functions
100
101 ; Ensures that positions are given as '((azimuth elevation) ...) (in degrees), i.e.
102 ; 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).
103 ; 2) if list of list of two values, input is assumed to be '(azimuth elevation) coordinates and won't be transformed any further.
104 ; 3) if 3dc, input is transformed to '(azimuth elevation) coordinates.
105 (defun sox-hoaencode-auto-convert-positions (positions)
106     (cond 
107         ((subtypep (type-of positions) '3dc)  
108             (progn ; convert xyz->aed, keep azimuth and elevation only.
109                 (mat-trans 
110                     (butlast 
111                         (mat-trans 
112                             (mapcar 
113                                 (lambda (xyz) (multiple-value-list (om:xyz->aed (first xyz) (second xyz) (third xyz)))) 
114                                 (mat-trans (list (x-points positions) (y-points positions) (z-points positions)))
115                             ))))))
116         ((subtypep (type-of positions) 'list) 
117             (cond
118                 ((= 2 (length (first positions)))
119                     positions)
120                 ((= 3 (length (first positions))) 
121                     (mat-trans 
122                         (butlast 
123                             (mat-trans 
124                                 (mapcar 
125                                     (lambda (xyz) (multiple-value-list (om:xyz->aed (first xyz) (second xyz) (third xyz)))) 
126                                     positions
127                                 )))))
128                 (t (error "Input must contain lists of length 2 (ae) or 3 (xyz)."))
129             ))
130         (t (error "Input must be 3dc or list."))))
131
132 ; High-level API
133 ; Returns the gain value (linear, -1 to 1) for a single ACN-channel
134 (defun sox-hoaencode-gain-single-component (order degree azimuth_deg elevation_deg)
135     (let
136         (
137             ; It is assumed that azimuth_deg follows the implementation details of SpatDIF, 
138             ; where azimuth_deg runs counterclockwise. 
139             ; However, the formulas inside assume that azimuth_deg runs clockwise, i.e. 
140             ; the sign of azimuth_deg must be inverted.
141             (azimuth_deg_inverted (* -1 azimuth_deg))
142         )
143         (* 
144             (sox-hoaencode-sn3d-factor order degree) 
145             (sox-hoaencode-azimuth-factor degree azimuth_deg_inverted) 
146             (sox-hoaencode-elevation-factor order degree elevation_deg)))
147     )
148
149 ; Returns the gain values for all components at a specific order
150 (defun sox-hoaencode-gains-by-order (order azimuth_deg elevation_deg)
151     (loop for degree from (* -1 order) to order collect 
152         (sox-hoaencode-gain-single-component order degree azimuth_deg elevation_deg)))
153
154 ; Returns the gain values for all components up to a specific order
155 (defun sox-hoaencode-gains-up-to-order (order azimuth_deg elevation_deg)
156     (sox-hoaencode-double-to-float 
157         (flatten
158             (loop for ord from 0 to order collect 
159                 (sox-hoaencode-gains-by-order ord azimuth_deg elevation_deg)))))
160
161
162 (defclass! sox-hoaencode (sox-input)
163     (
164         (positions :accessor positions :initarg :positions :initform nil :documentation *sox-hoaencode-positions-doc*)
165         (order :accessor order :initarg :order :initform nil :documentation *sox-hoaencode-order-doc*)
166     )
167     (:icon 100)
168     (:documentation "Sox-hoaencode transforms <sound> into a <order>-th ambisonic (HOA) signal at <positions>.
169
170     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, ...).")
171 )
172
173 (defmethod initialize-instance :after ((self sox-hoaencode) &rest l)
174     (declare (ignore l))
175     (when (sound self)
176         (sox-init-sound self 'list)
177     )
178 )