2 * 2-channel UHJ Encoder
4 * Copyright (c) Chris Robinson <chris.kcat@gmail.com>
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
37 #include "alnumbers.h"
39 #include "opthelpers.h"
40 #include "phase_shifter.h"
45 #include "win_main_utf8.h"
50 struct SndFileDeleter
{
51 void operator()(SNDFILE
*sndfile
) { sf_close(sndfile
); }
53 using SndFilePtr
= std::unique_ptr
<SNDFILE
,SndFileDeleter
>;
56 using uint
= unsigned int;
58 constexpr uint BufferLineSize
{1024};
60 using FloatBufferLine
= std::array
<float,BufferLineSize
>;
61 using FloatBufferSpan
= al::span
<float,BufferLineSize
>;
65 constexpr static size_t sFilterDelay
{1024};
67 /* Delays and processing storage for the unfiltered signal. */
68 alignas(16) std::array
<float,BufferLineSize
+sFilterDelay
> mS
{};
69 alignas(16) std::array
<float,BufferLineSize
+sFilterDelay
> mD
{};
70 alignas(16) std::array
<float,BufferLineSize
+sFilterDelay
> mT
{};
71 alignas(16) std::array
<float,BufferLineSize
+sFilterDelay
> mQ
{};
73 /* History for the FIR filter. */
74 alignas(16) std::array
<float,sFilterDelay
*2 - 1> mWXHistory1
{};
75 alignas(16) std::array
<float,sFilterDelay
*2 - 1> mWXHistory2
{};
77 alignas(16) std::array
<float,BufferLineSize
+ sFilterDelay
*2> mTemp
{};
79 void encode(const al::span
<FloatBufferLine
> OutSamples
,
80 const al::span
<FloatBufferLine
,4> InSamples
, const size_t SamplesToDo
);
82 DEF_NEWDEL(UhjEncoder
)
85 const PhaseShifterT
<UhjEncoder::sFilterDelay
*2> PShift
{};
88 /* Encoding UHJ from B-Format is done as:
90 * S = 0.9396926*W + 0.1855740*X
91 * D = j(-0.3420201*W + 0.5098604*X) + 0.6554516*Y
95 * T = j(-0.1432*W + 0.6512*X) - 0.7071068*Y
98 * where j is a wide-band +90 degree phase shift. T is excluded from 2-channel
99 * output, and Q is excluded from 2- and 3-channel output.
101 void UhjEncoder::encode(const al::span
<FloatBufferLine
> OutSamples
,
102 const al::span
<FloatBufferLine
,4> InSamples
, const size_t SamplesToDo
)
104 const float *RESTRICT winput
{al::assume_aligned
<16>(InSamples
[0].data())};
105 const float *RESTRICT xinput
{al::assume_aligned
<16>(InSamples
[1].data())};
106 const float *RESTRICT yinput
{al::assume_aligned
<16>(InSamples
[2].data())};
107 const float *RESTRICT zinput
{al::assume_aligned
<16>(InSamples
[3].data())};
109 /* Combine the previously delayed S/D signal with the input. */
111 /* S = 0.9396926*W + 0.1855740*X */
112 auto miditer
= mS
.begin() + sFilterDelay
;
113 std::transform(winput
, winput
+SamplesToDo
, xinput
, miditer
,
114 [](const float w
, const float x
) noexcept
-> float
115 { return 0.9396926f
*w
+ 0.1855740f
*x
; });
117 /* D = 0.6554516*Y */
118 auto sideiter
= mD
.begin() + sFilterDelay
;
119 std::transform(yinput
, yinput
+SamplesToDo
, sideiter
,
120 [](const float y
) noexcept
-> float { return 0.6554516f
*y
; });
122 /* D += j(-0.3420201*W + 0.5098604*X) */
123 auto tmpiter
= std::copy(mWXHistory1
.cbegin(), mWXHistory1
.cend(), mTemp
.begin());
124 std::transform(winput
, winput
+SamplesToDo
, xinput
, tmpiter
,
125 [](const float w
, const float x
) noexcept
-> float
126 { return -0.3420201f
*w
+ 0.5098604f
*x
; });
127 std::copy_n(mTemp
.cbegin()+SamplesToDo
, mWXHistory1
.size(), mWXHistory1
.begin());
128 PShift
.processAccum({mD
.data(), SamplesToDo
}, mTemp
.data());
130 /* Left = (S + D)/2.0 */
131 float *RESTRICT left
{al::assume_aligned
<16>(OutSamples
[0].data())};
132 for(size_t i
{0};i
< SamplesToDo
;i
++)
133 left
[i
] = (mS
[i
] + mD
[i
]) * 0.5f
;
134 /* Right = (S - D)/2.0 */
135 float *RESTRICT right
{al::assume_aligned
<16>(OutSamples
[1].data())};
136 for(size_t i
{0};i
< SamplesToDo
;i
++)
137 right
[i
] = (mS
[i
] - mD
[i
]) * 0.5f
;
139 if(OutSamples
.size() > 2)
141 /* T = -0.7071068*Y */
142 sideiter
= mT
.begin() + sFilterDelay
;
143 std::transform(yinput
, yinput
+SamplesToDo
, sideiter
,
144 [](const float y
) noexcept
-> float { return -0.7071068f
*y
; });
146 /* T += j(-0.1432*W + 0.6512*X) */
147 tmpiter
= std::copy(mWXHistory2
.cbegin(), mWXHistory2
.cend(), mTemp
.begin());
148 std::transform(winput
, winput
+SamplesToDo
, xinput
, tmpiter
,
149 [](const float w
, const float x
) noexcept
-> float
150 { return -0.1432f
*w
+ 0.6512f
*x
; });
151 std::copy_n(mTemp
.cbegin()+SamplesToDo
, mWXHistory2
.size(), mWXHistory2
.begin());
152 PShift
.processAccum({mT
.data(), SamplesToDo
}, mTemp
.data());
154 float *RESTRICT t
{al::assume_aligned
<16>(OutSamples
[2].data())};
155 for(size_t i
{0};i
< SamplesToDo
;i
++)
158 if(OutSamples
.size() > 3)
161 sideiter
= mQ
.begin() + sFilterDelay
;
162 std::transform(zinput
, zinput
+SamplesToDo
, sideiter
,
163 [](const float z
) noexcept
-> float { return 0.9772f
*z
; });
165 float *RESTRICT q
{al::assume_aligned
<16>(OutSamples
[3].data())};
166 for(size_t i
{0};i
< SamplesToDo
;i
++)
170 /* Copy the future samples to the front for next time. */
171 std::copy(mS
.cbegin()+SamplesToDo
, mS
.cbegin()+SamplesToDo
+sFilterDelay
, mS
.begin());
172 std::copy(mD
.cbegin()+SamplesToDo
, mD
.cbegin()+SamplesToDo
+sFilterDelay
, mD
.begin());
173 std::copy(mT
.cbegin()+SamplesToDo
, mT
.cbegin()+SamplesToDo
+sFilterDelay
, mT
.begin());
174 std::copy(mQ
.cbegin()+SamplesToDo
, mQ
.cbegin()+SamplesToDo
+sFilterDelay
, mQ
.begin());
184 /* Azimuth is counter-clockwise. */
185 constexpr SpeakerPos StereoMap
[2]{
186 { SF_CHANNEL_MAP_LEFT
, 30.0f
, 0.0f
},
187 { SF_CHANNEL_MAP_RIGHT
, -30.0f
, 0.0f
},
189 { SF_CHANNEL_MAP_LEFT
, 45.0f
, 0.0f
},
190 { SF_CHANNEL_MAP_RIGHT
, -45.0f
, 0.0f
},
191 { SF_CHANNEL_MAP_REAR_LEFT
, 135.0f
, 0.0f
},
192 { SF_CHANNEL_MAP_REAR_RIGHT
, -135.0f
, 0.0f
},
194 { SF_CHANNEL_MAP_LEFT
, 30.0f
, 0.0f
},
195 { SF_CHANNEL_MAP_RIGHT
, -30.0f
, 0.0f
},
196 { SF_CHANNEL_MAP_CENTER
, 0.0f
, 0.0f
},
197 { SF_CHANNEL_MAP_LFE
, 0.0f
, 0.0f
},
198 { SF_CHANNEL_MAP_SIDE_LEFT
, 110.0f
, 0.0f
},
199 { SF_CHANNEL_MAP_SIDE_RIGHT
, -110.0f
, 0.0f
},
201 { SF_CHANNEL_MAP_LEFT
, 30.0f
, 0.0f
},
202 { SF_CHANNEL_MAP_RIGHT
, -30.0f
, 0.0f
},
203 { SF_CHANNEL_MAP_CENTER
, 0.0f
, 0.0f
},
204 { SF_CHANNEL_MAP_LFE
, 0.0f
, 0.0f
},
205 { SF_CHANNEL_MAP_REAR_LEFT
, 110.0f
, 0.0f
},
206 { SF_CHANNEL_MAP_REAR_RIGHT
, -110.0f
, 0.0f
},
208 { SF_CHANNEL_MAP_LEFT
, 30.0f
, 0.0f
},
209 { SF_CHANNEL_MAP_RIGHT
, -30.0f
, 0.0f
},
210 { SF_CHANNEL_MAP_CENTER
, 0.0f
, 0.0f
},
211 { SF_CHANNEL_MAP_LFE
, 0.0f
, 0.0f
},
212 { SF_CHANNEL_MAP_REAR_LEFT
, 150.0f
, 0.0f
},
213 { SF_CHANNEL_MAP_REAR_RIGHT
, -150.0f
, 0.0f
},
214 { SF_CHANNEL_MAP_SIDE_LEFT
, 90.0f
, 0.0f
},
215 { SF_CHANNEL_MAP_SIDE_RIGHT
, -90.0f
, 0.0f
},
217 { SF_CHANNEL_MAP_LEFT
, 30.0f
, 0.0f
},
218 { SF_CHANNEL_MAP_RIGHT
, -30.0f
, 0.0f
},
219 { SF_CHANNEL_MAP_CENTER
, 0.0f
, 0.0f
},
220 { SF_CHANNEL_MAP_LFE
, 0.0f
, 0.0f
},
221 { SF_CHANNEL_MAP_REAR_LEFT
, 150.0f
, 0.0f
},
222 { SF_CHANNEL_MAP_REAR_RIGHT
, -150.0f
, 0.0f
},
223 { SF_CHANNEL_MAP_SIDE_LEFT
, 90.0f
, 0.0f
},
224 { SF_CHANNEL_MAP_SIDE_RIGHT
, -90.0f
, 0.0f
},
225 { SF_CHANNEL_MAP_TOP_FRONT_LEFT
, 45.0f
, 35.0f
},
226 { SF_CHANNEL_MAP_TOP_FRONT_RIGHT
, -45.0f
, 35.0f
},
227 { SF_CHANNEL_MAP_TOP_REAR_LEFT
, 135.0f
, 35.0f
},
228 { SF_CHANNEL_MAP_TOP_REAR_RIGHT
, -135.0f
, 35.0f
},
231 constexpr auto GenCoeffs(double x
/*+front*/, double y
/*+left*/, double z
/*+up*/) noexcept
233 /* Coefficients are +3dB of FuMa. */
234 return std::array
<float,4>{{
236 static_cast<float>(al::numbers::sqrt2
* x
),
237 static_cast<float>(al::numbers::sqrt2
* y
),
238 static_cast<float>(al::numbers::sqrt2
* z
)
245 int main(int argc
, char **argv
)
247 if(argc
< 2 || std::strcmp(argv
[1], "-h") == 0 || std::strcmp(argv
[1], "--help") == 0)
249 printf("Usage: %s <infile...>\n\n", argv
[0]);
254 size_t num_files
{0}, num_encoded
{0};
255 for(int fidx
{1};fidx
< argc
;++fidx
)
257 if(strcmp(argv
[fidx
], "-bhj") == 0)
262 if(strcmp(argv
[fidx
], "-thj") == 0)
267 if(strcmp(argv
[fidx
], "-phj") == 0)
274 std::string outname
{argv
[fidx
]};
275 size_t lastslash
{outname
.find_last_of('/')};
276 if(lastslash
!= std::string::npos
)
277 outname
.erase(0, lastslash
+1);
278 size_t extpos
{outname
.find_last_of('.')};
279 if(extpos
!= std::string::npos
)
280 outname
.resize(extpos
);
281 outname
+= ".uhj.flac";
284 SndFilePtr infile
{sf_open(argv
[fidx
], SFM_READ
, &ininfo
)};
287 fprintf(stderr
, "Failed to open %s\n", argv
[fidx
]);
290 printf("Converting %s to %s...\n", argv
[fidx
], outname
.c_str());
292 /* Work out the channel map, preferably using the actual channel map
293 * from the file/format, but falling back to assuming WFX order.
295 * TODO: Map indices when the channel order differs from the virtual
296 * speaker position maps.
298 al::span
<const SpeakerPos
> spkrs
;
299 auto chanmap
= std::vector
<int>(static_cast<uint
>(ininfo
.channels
), SF_CHANNEL_MAP_INVALID
);
300 if(sf_command(infile
.get(), SFC_GET_CHANNEL_MAP_INFO
, chanmap
.data(),
301 ininfo
.channels
*int{sizeof(int)}) == SF_TRUE
)
303 static const std::array
<int,2> stereomap
{{SF_CHANNEL_MAP_LEFT
, SF_CHANNEL_MAP_RIGHT
}};
304 static const std::array
<int,4> quadmap
{{SF_CHANNEL_MAP_LEFT
, SF_CHANNEL_MAP_RIGHT
,
305 SF_CHANNEL_MAP_REAR_LEFT
, SF_CHANNEL_MAP_REAR_RIGHT
}};
306 static const std::array
<int,6> x51map
{{SF_CHANNEL_MAP_LEFT
, SF_CHANNEL_MAP_RIGHT
,
307 SF_CHANNEL_MAP_CENTER
, SF_CHANNEL_MAP_LFE
,
308 SF_CHANNEL_MAP_SIDE_LEFT
, SF_CHANNEL_MAP_SIDE_RIGHT
}};
309 static const std::array
<int,6> x51rearmap
{{SF_CHANNEL_MAP_LEFT
, SF_CHANNEL_MAP_RIGHT
,
310 SF_CHANNEL_MAP_CENTER
, SF_CHANNEL_MAP_LFE
,
311 SF_CHANNEL_MAP_REAR_LEFT
, SF_CHANNEL_MAP_REAR_RIGHT
}};
312 static const std::array
<int,8> x71map
{{SF_CHANNEL_MAP_LEFT
, SF_CHANNEL_MAP_RIGHT
,
313 SF_CHANNEL_MAP_CENTER
, SF_CHANNEL_MAP_LFE
,
314 SF_CHANNEL_MAP_REAR_LEFT
, SF_CHANNEL_MAP_REAR_RIGHT
,
315 SF_CHANNEL_MAP_SIDE_LEFT
, SF_CHANNEL_MAP_SIDE_RIGHT
}};
316 static const std::array
<int,12> x714map
{{SF_CHANNEL_MAP_LEFT
, SF_CHANNEL_MAP_RIGHT
,
317 SF_CHANNEL_MAP_CENTER
, SF_CHANNEL_MAP_LFE
,
318 SF_CHANNEL_MAP_REAR_LEFT
, SF_CHANNEL_MAP_REAR_RIGHT
,
319 SF_CHANNEL_MAP_SIDE_LEFT
, SF_CHANNEL_MAP_SIDE_RIGHT
,
320 SF_CHANNEL_MAP_TOP_FRONT_LEFT
, SF_CHANNEL_MAP_TOP_FRONT_RIGHT
,
321 SF_CHANNEL_MAP_TOP_REAR_LEFT
, SF_CHANNEL_MAP_TOP_REAR_RIGHT
}};
322 static const std::array
<int,3> ambi2dmap
{{SF_CHANNEL_MAP_AMBISONIC_B_W
,
323 SF_CHANNEL_MAP_AMBISONIC_B_X
, SF_CHANNEL_MAP_AMBISONIC_B_Y
}};
324 static const std::array
<int,4> ambi3dmap
{{SF_CHANNEL_MAP_AMBISONIC_B_W
,
325 SF_CHANNEL_MAP_AMBISONIC_B_X
, SF_CHANNEL_MAP_AMBISONIC_B_Y
,
326 SF_CHANNEL_MAP_AMBISONIC_B_Z
}};
328 auto match_chanmap
= [](const al::span
<int> a
, const al::span
<const int> b
) -> bool
330 return a
.size() == b
.size()
331 && std::mismatch(a
.begin(), a
.end(), b
.begin(), b
.end()).first
== a
.end();
333 if(match_chanmap(chanmap
, stereomap
))
335 else if(match_chanmap(chanmap
, quadmap
))
337 else if(match_chanmap(chanmap
, x51map
))
339 else if(match_chanmap(chanmap
, x51rearmap
))
341 else if(match_chanmap(chanmap
, x71map
))
343 else if(match_chanmap(chanmap
, x714map
))
345 else if(match_chanmap(chanmap
, ambi2dmap
) || match_chanmap(chanmap
, ambi3dmap
))
352 if(chanmap
.size() > 0)
354 mapstr
= std::to_string(chanmap
[0]);
355 for(int idx
: al::span
<int>{chanmap
}.subspan
<1>())
358 mapstr
+= std::to_string(idx
);
361 fprintf(stderr
, " ... %zu channels not supported (map: %s)\n", chanmap
.size(),
366 else if(ininfo
.channels
== 2)
368 fprintf(stderr
, " ... assuming WFX order stereo\n");
371 else if(ininfo
.channels
== 6)
373 fprintf(stderr
, " ... assuming WFX order 5.1\n");
376 else if(ininfo
.channels
== 8)
378 fprintf(stderr
, " ... assuming WFX order 7.1\n");
383 fprintf(stderr
, " ... unmapped %d-channel audio not supported\n", ininfo
.channels
);
388 outinfo
.frames
= ininfo
.frames
;
389 outinfo
.samplerate
= ininfo
.samplerate
;
390 outinfo
.channels
= static_cast<int>(uhjchans
);
391 outinfo
.format
= SF_FORMAT_PCM_24
| SF_FORMAT_FLAC
;
392 SndFilePtr outfile
{sf_open(outname
.c_str(), SFM_WRITE
, &outinfo
)};
395 fprintf(stderr
, " ... failed to create %s\n", outname
.c_str());
399 auto encoder
= std::make_unique
<UhjEncoder
>();
400 auto splbuf
= al::vector
<FloatBufferLine
, 16>(static_cast<uint
>(9+ininfo
.channels
)+uhjchans
);
401 auto ambmem
= al::span
<FloatBufferLine
,4>{&splbuf
[0], 4};
402 auto encmem
= al::span
<FloatBufferLine
,4>{&splbuf
[4], 4};
403 auto srcmem
= al::span
<float,BufferLineSize
>{splbuf
[8].data(), BufferLineSize
};
404 auto outmem
= al::span
<float>{splbuf
[9].data(), BufferLineSize
*uhjchans
};
406 /* A number of initial samples need to be skipped to cut the lead-in
407 * from the all-pass filter delay. The same number of samples need to
408 * be fed through the encoder after reaching the end of the input file
409 * to ensure none of the original input is lost.
411 size_t total_wrote
{0};
412 size_t LeadIn
{UhjEncoder::sFilterDelay
};
413 sf_count_t LeadOut
{UhjEncoder::sFilterDelay
};
414 while(LeadIn
> 0 || LeadOut
> 0)
416 auto inmem
= outmem
.data() + outmem
.size();
417 auto sgot
= sf_readf_float(infile
.get(), inmem
, BufferLineSize
);
419 sgot
= std::max
<sf_count_t
>(sgot
, 0);
420 if(sgot
< BufferLineSize
)
422 const sf_count_t remaining
{std::min(BufferLineSize
- sgot
, LeadOut
)};
423 std::fill_n(inmem
+ sgot
*ininfo
.channels
, remaining
*ininfo
.channels
, 0.0f
);
425 LeadOut
-= remaining
;
428 for(auto&& buf
: ambmem
)
431 auto got
= static_cast<size_t>(sgot
);
434 /* B-Format is already in the correct order. It just needs a
437 constexpr float scale
{al::numbers::sqrt2_v
<float>};
438 const size_t chans
{std::min
<size_t>(static_cast<uint
>(ininfo
.channels
), 4u)};
439 for(size_t c
{0};c
< chans
;++c
)
441 for(size_t i
{0};i
< got
;++i
)
442 ambmem
[c
][i
] = inmem
[i
*static_cast<uint
>(ininfo
.channels
)] * scale
;
446 else for(auto&& spkr
: spkrs
)
448 /* Skip LFE. Or mix directly into W? Or W+X? */
449 if(spkr
.mChannelID
== SF_CHANNEL_MAP_LFE
)
455 for(size_t i
{0};i
< got
;++i
)
456 srcmem
[i
] = inmem
[i
* static_cast<uint
>(ininfo
.channels
)];
459 constexpr auto Deg2Rad
= al::numbers::pi
/ 180.0;
460 const auto coeffs
= GenCoeffs(
461 std::cos(spkr
.mAzimuth
*Deg2Rad
) * std::cos(spkr
.mElevation
*Deg2Rad
),
462 std::sin(spkr
.mAzimuth
*Deg2Rad
) * std::cos(spkr
.mElevation
*Deg2Rad
),
463 std::sin(spkr
.mElevation
*Deg2Rad
));
464 for(size_t c
{0};c
< 4;++c
)
466 for(size_t i
{0};i
< got
;++i
)
467 ambmem
[c
][i
] += srcmem
[i
] * coeffs
[c
];
471 encoder
->encode(encmem
.subspan(0, uhjchans
), ambmem
, got
);
479 for(size_t c
{0};c
< uhjchans
;++c
)
481 constexpr float max_val
{8388607.0f
/ 8388608.0f
};
482 auto clamp
= [](float v
, float mn
, float mx
) noexcept
483 { return std::min(std::max(v
, mn
), mx
); };
484 for(size_t i
{0};i
< got
;++i
)
485 outmem
[i
*uhjchans
+ c
] = clamp(encmem
[c
][LeadIn
+i
], -1.0f
, max_val
);
489 sf_count_t wrote
{sf_writef_float(outfile
.get(), outmem
.data(),
490 static_cast<sf_count_t
>(got
))};
492 fprintf(stderr
, " ... failed to write samples: %d\n", sf_error(outfile
.get()));
494 total_wrote
+= static_cast<size_t>(wrote
);
496 printf(" ... wrote %zu samples (%" PRId64
").\n", total_wrote
, int64_t{ininfo
.frames
});
500 fprintf(stderr
, "Failed to encode any input files\n");
501 else if(num_encoded
< num_files
)
502 fprintf(stderr
, "Encoded %zu of %zu files\n", num_encoded
, num_files
);
504 printf("Encoded %s%zu file%s\n", (num_encoded
> 1) ? "all " : "", num_encoded
,
505 (num_encoded
== 1) ? "" : "s");