Work-in-progress repo for ambisonics extensions for OM-SoX
Alexander Nguyen
11 hours ago d6bb375789821f4e4e175465f50e77e8a6b1aaf7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
;Authors: 
; A. Nguyen, 2025
; M. Schumacher, 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? Yes, om:flat
; 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 
 
(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)
"This is a placeholder for documentation"
    (ecase order
        (0 1)
        (1 1)
        (2 
            (ecase (abs degree)
                (0 1)
                (1 (/ (sqrt 3) 3))
                (2 (/ (sqrt 3) 6))
            ))
        (3 
            (ecase (abs degree)
                (0 1)
                (1 (/ (sqrt 6) 6))
                (2 (/ (sqrt 15) 30))
                (3 (/ (sqrt 10) 60))
            )
        )
        (4 
            (ecase (abs degree)
                (0 1)
                (1 (/ (sqrt 10) 10))
                (2 (/ (sqrt 5) 30))
                (3 (/ (sqrt 70) 420))
                (4 (/ (sqrt 35) 840))
            )
        )
        (5
            (ecase (abs degree)
                (0 1)
                (1 (/ (sqrt 15) 15))
                (2 (/ (sqrt 105) 210))
                (3 (/ (sqrt 70) 840))
                (4 (/ (sqrt 35) 2520))
                (5 (/ (sqrt 14) 5040))
            )
        )
        (6
            (ecase (abs degree)
                (0 1)
                (1 (/ (sqrt 21) 21))
                (2 (/ (sqrt 210) 420))
                (3 (/ (sqrt 210) 2520))
                (4 (/ (sqrt 7) 2520))
                (5 (/ (sqrt 154) 55440))
                (6 (/ (sqrt 462) 332640))
            )
        )
        (7
            (ecase (abs degree)
                (0 1)
                (1 (/ (sqrt 7) 14))
                (2 (/ (sqrt 42) 252))
                (3 (/ (sqrt 21) 1260))
                (4 (/ (sqrt 231) 27720))
                (5 (/ (sqrt 231) 166320))
                (6 (/ (sqrt 6006) 4324320))
                (7 (/ (sqrt 429) 4324320))
            )
        )
    )
)
 
(defun sox-hoaencode-azimuth-factor (degree theta_deg)
"This is a placeholder for documentation"
    (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)
"This is a placeholder for documentation"
    (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 (* (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)))
                ))
            (4 
                (ecase (abs degree)
                    (0 (+ (/ (* 35 (expt (cos phi) 4)) 8) (- (* 5 (expt (cos phi) 2))) 1))
                    (1 (- (/ (* (sin (* 2 phi)) (- (/ (* 35 (expt (cos phi) 2)) 2) 10)) 2)))
                    (2 (- (/ (* (* 15 (expt (cos phi) 2)) (- (* 7 (expt (cos phi) 2)) 6)) 2)))
                    (3 (* 105 (expt (cos phi) 3) (sin phi) ))
                    (4 (* 105 (expt (cos phi) 4)))
                ))
            (5 
                (ecase (abs degree)
                    (0    (/ (*     (sin phi) (+ (- (* 63 (expt (sin phi) 4)) (* 70 (expt (sin phi) 2))) 15)) 8))
                    (1    (/ (*  15 (sin phi) (+ (- (* 21 (expt (cos phi) 4)) (* 28 (expt (cos phi) 2)))  8)) 8))
                    (2 (- (/ (* 105 (sin phi) (+ (- (*  3 (expt (sin phi) 4)) (*  4 (expt (sin phi) 2)))  1)) 2)))
                    (3 (- (/ (* 105 (expt (cos phi) 3) (- (* 9 (expt (cos phi) 2)) 8))                        2)))
                    (4       (* 945 (sin phi) (expt (- (expt (sin phi) 2) 1) 2)))
                    (5       (* 945 (expt (cos phi) 5)))
                ))
            (6 
                (ecase (abs degree)
                    (0 (+ (- (/ (* 231 (expt (cos phi) 6)) 16)) (/ (* 189 (expt (cos phi) 4)) 8) (- (/ (* 21 (expt (cos phi) 2)) 2)) 1))
                    (1 (/ (* 21 (sin (* 2 phi)) (+ (* 33 (expt (sin phi) 4)) (- (* 30 (expt (sin phi) 2))) 5)) 16))
                    (2 (/ (* 105 (expt (cos phi) 2) (+ (* 33 (expt (cos phi) 4)) (- (* 48 (expt (cos phi) 2))) 16)) 8))
                    (3 (- (/ (* 315 (expt (cos phi) 3) (sin phi)) (- (* 11 (expt (cos phi) 2)) 8)) 2))
                    (4 (- (/ (* 945 (expt (cos phi) 4) (- (* 11 (expt (cos phi) 2)) 10)) 2)))
                    (5 (* 10395 (expt (cos phi) 5) (sin phi)))
                    (6 (* 10395 (expt (cos phi) 6)))
                ))
            (7
                (ecase (abs degree)
                    (0    (/ (* (sin phi) (+ (* 429 (expt (sin phi) 6)) (- (* 693 (expt (sin phi) 4))) (* 315 (expt (sin phi) 2)) (- 35))) 16))
                    (1 (- (/ (* (* 7 (cos phi)) (+ (* 429 (expt (cos phi) 6)) (- (* 792 (expt (cos phi) 4))) (* 432 (expt (cos phi) 2)) (- 64))) 16)))
                    (2 (- (/ (* 63 (sin phi) (- (expt (sin phi) 2) 1) (+ (* 143 (expt (sin phi) 4)) (- (* 110 (expt (sin phi) 2))) 15)) 8)))
                    (3    (/ (* (* 315 (expt (cos phi) 3)) (+ (* 143 (expt (cos phi) 4)) (- (* 220 (expt (cos phi) 2))) 80)) 8))
                    (4    (/ (* 3465 (sin phi) (- (* 13 (expt (sin phi) 2)) 3) (expt (- (expt (sin phi) 2) 1) 2)) 2))
                    (5 (- (/ (* 10395 (expt (cos phi) 5) (- (* 13 (expt (cos phi) 2)) 12)) 2)))
                    (6 (* -135135 (sin phi) (expt (- (expt (sin phi) 2) 1) 3)))
                    (7 (* 135135 (expt (cos phi) 7)))
                ))
        )
    ))
 
 
 
; ####### High-level API ########## 
 
 
(defun sox-hoaencode-gain-single-component (order degree azimuth_deg elevation_deg)
  "Returns the gain value (linear, -1 to 1) for a single ACN-channel"
    (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)))
    )
 
; (sox-hoaencode-gain-single-component 1 1 45 0)
; @AN: what is the difference between "order" and "degree"?
 
 
(defun sox-hoaencode-gains-by-order (order azimuth_deg elevation_deg)
  "Returns the gain values for all components at a specific order"
    (loop for degree from (* -1 order) to order collect 
        (sox-hoaencode-gain-single-component order degree azimuth_deg elevation_deg)))
 
 
(defun sox-hoaencode-gains-up-to-order (order azimuth_deg elevation_deg)
  "Returns the gain values for all components up to a specific order"
    (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)
    (
     (gains :accessor gains :initarg :gains :initform nil :documentation *sox-gain-doc*)
     (azimuth :accessor azimuth :initarg :azimuth :initform 0 :documentation "azimuth angle in degrees")
     (elevation :accessor elevation :initarg :elevation :initform 0 :documentation "elevation angle in degrees")
     (order :accessor order :initarg :order :initform 3 :documentation *sox-hoaencode-order-doc*)
    )
    (:icon 100)
    (:documentation "Sox-hoaencode encodes <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 'atom)
    )
)