Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 428772eb87 | |||
| 6bc2c28856 | |||
| 704ea0897b | |||
| c6859e6b02 |
+1
-1
@@ -11,7 +11,7 @@ This is research code, not a complete, standards-compliant, or production-grade
|
|||||||
## Current features
|
## Current features
|
||||||
|
|
||||||
- Scan common contiguous EMDF containers in E-AC-3 sync frames.
|
- Scan common contiguous EMDF containers in E-AC-3 sync frames.
|
||||||
- Parse ID14 dense JOC parameters, Huffman data, differential matrices, and `joc_clipgain`.
|
- Parse ID14 dense / sparse JOC parameters, Huffman data, differential matrices, and `joc_clipgain`.
|
||||||
- Parse ID11 OAMD position updates and build object trajectories.
|
- Parse ID11 OAMD position updates and build object trajectories.
|
||||||
- Reconstruct LFE plus 15 object channels through analysis QMF, parameter interpolation, the object matrix, and inverse QMF.
|
- Reconstruct LFE plus 15 object channels through analysis QMF, parameter interpolation, the object matrix, and inverse QMF.
|
||||||
- Write a 25-channel ADM BWF: a 10-channel 7.1.2 bed (silent except for LFE) plus 15 objects.
|
- Write a 25-channel ADM BWF: a 10-channel 7.1.2 bed (silent except for LFE) plus 15 objects.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
## 当前功能
|
## 当前功能
|
||||||
|
|
||||||
- 扫描 E-AC-3 同步帧中的常见连续 EMDF 容器;
|
- 扫描 E-AC-3 同步帧中的常见连续 EMDF 容器;
|
||||||
- 解析 ID14 dense JOC 参数、Huffman 数据、差分矩阵与 `joc_clipgain`;
|
- 解析 ID14 dense / sparse JOC 参数、Huffman 数据、差分矩阵与 `joc_clipgain`;
|
||||||
- 解析 ID11 OAMD 位置更新并生成对象轨迹;
|
- 解析 ID11 OAMD 位置更新并生成对象轨迹;
|
||||||
- 通过 analysis QMF、参数插值、对象矩阵和 inverse QMF 重建 LFE + 15 路对象 PCM;
|
- 通过 analysis QMF、参数插值、对象矩阵和 inverse QMF 重建 LFE + 15 路对象 PCM;
|
||||||
- 输出 25 声道 ADM BWF:10 声道 7.1.2 bed(除 LFE 外静音)+ 15 个对象;
|
- 输出 25 声道 ADM BWF:10 声道 7.1.2 bed(除 LFE 外静音)+ 15 个对象;
|
||||||
|
|||||||
+3
-3
@@ -89,12 +89,12 @@ $$
|
|||||||
|
|
||||||
### 2.2 Sparse differential reconstruction
|
### 2.2 Sparse differential reconstruction
|
||||||
|
|
||||||
Let $I_{o,d,p}$ be the `joc_channel_idx` symbol (IDX), $V_{o,d,p}$ the `joc_vec` symbol (VEC), and $N_c\in\{5,7\}$ the number of core channels. Each parameter band has exactly one active channel:
|
Let $I_{o,d,p}$ be the `joc_channel_idx` symbol (IDX), $V_{o,d,p}$ the `joc_vec` symbol (VEC), and $N_c\in\lbrace5,7\rbrace$ the number of core channels. Each parameter band has exactly one active channel:
|
||||||
|
|
||||||
$$
|
$$
|
||||||
A_{o,d,p}=
|
A_{o,d,p}=
|
||||||
\begin{cases}
|
\begin{cases}
|
||||||
I_{o,d,0}, & p=0,\\[2pt]
|
I_{o,d,0}, & p=0,\\
|
||||||
\left(A_{o,d,p-1}+I_{o,d,p}\right)\bmod N_c, & p>0,
|
\left(A_{o,d,p-1}+I_{o,d,p}\right)\bmod N_c, & p>0,
|
||||||
\end{cases}
|
\end{cases}
|
||||||
$$
|
$$
|
||||||
@@ -122,7 +122,7 @@ The accumulator is **not** reset when the active channel changes. The complete m
|
|||||||
$$
|
$$
|
||||||
Q_{o,d,c,p}=
|
Q_{o,d,c,p}=
|
||||||
\begin{cases}
|
\begin{cases}
|
||||||
\kappa_{o,d,p}, & c=A_{o,d,p},\\[2pt]
|
\kappa_{o,d,p}, & c=A_{o,d,p},\\
|
||||||
\dfrac{N_q}{2}, & c\neq A_{o,d,p}.
|
\dfrac{N_q}{2}, & c\neq A_{o,d,p}.
|
||||||
\end{cases}
|
\end{cases}
|
||||||
$$
|
$$
|
||||||
|
|||||||
+3
-3
@@ -89,12 +89,12 @@ $$
|
|||||||
|
|
||||||
### 2.2 Sparse 差分还原
|
### 2.2 Sparse 差分还原
|
||||||
|
|
||||||
令 $I_{o,d,p}$ 为 `joc_channel_idx` 符号(IDX),$V_{o,d,p}$ 为 `joc_vec` 符号(VEC),$N_c\in\{5,7\}$ 为核心声道数。每参数带只有一个 active 声道
|
令 $I_{o,d,p}$ 为 `joc_channel_idx` 符号(IDX), $V_{o,d,p}$ 为 `joc_vec` 符号(VEC), $N_c\in\lbrace5,7\rbrace$ 为核心声道数。每参数带只有一个 active 声道
|
||||||
|
|
||||||
$$
|
$$
|
||||||
A_{o,d,p}=
|
A_{o,d,p}=
|
||||||
\begin{cases}
|
\begin{cases}
|
||||||
I_{o,d,0}, & p=0,\\[2pt]
|
I_{o,d,0}, & p=0,\\
|
||||||
\left(A_{o,d,p-1}+I_{o,d,p}\right)\bmod N_c, & p>0,
|
\left(A_{o,d,p-1}+I_{o,d,p}\right)\bmod N_c, & p>0,
|
||||||
\end{cases}
|
\end{cases}
|
||||||
$$
|
$$
|
||||||
@@ -122,7 +122,7 @@ active 声道切换时累加器**不**重置。完整矩阵为
|
|||||||
$$
|
$$
|
||||||
Q_{o,d,c,p}=
|
Q_{o,d,c,p}=
|
||||||
\begin{cases}
|
\begin{cases}
|
||||||
\kappa_{o,d,p}, & c=A_{o,d,p},\\[2pt]
|
\kappa_{o,d,p}, & c=A_{o,d,p},\\
|
||||||
\dfrac{N_q}{2}, & c\neq A_{o,d,p}.
|
\dfrac{N_q}{2}, & c\neq A_{o,d,p}.
|
||||||
\end{cases}
|
\end{cases}
|
||||||
$$
|
$$
|
||||||
|
|||||||
@@ -64,6 +64,15 @@ EJOC_API ejoc_renderer_handle EJOC_CALL ejoc_renderer_create(void);
|
|||||||
EJOC_API void EJOC_CALL ejoc_renderer_destroy(ejoc_renderer_handle handle);
|
EJOC_API void EJOC_CALL ejoc_renderer_destroy(ejoc_renderer_handle handle);
|
||||||
EJOC_API int EJOC_CALL ejoc_renderer_reset(ejoc_renderer_handle handle);
|
EJOC_API int EJOC_CALL ejoc_renderer_reset(ejoc_renderer_handle handle);
|
||||||
EJOC_API int EJOC_CALL ejoc_renderer_set_threads(ejoc_renderer_handle handle, uint32_t total_threads);
|
EJOC_API int EJOC_CALL ejoc_renderer_set_threads(ejoc_renderer_handle handle, uint32_t total_threads);
|
||||||
|
/*
|
||||||
|
Enables or disables the Ls/Rs band-0 21-tap DC compensation. The caller derives
|
||||||
|
it from the JOC downmix configuration: only configurations 3 and 4 enable the
|
||||||
|
filter. When disabled, band 0 keeps the common per-band processing (surround
|
||||||
|
delay plus -j rotation) instead of being overwritten by the FIR. The delay line
|
||||||
|
and DC history advance either way, so the flag may change between frames.
|
||||||
|
Defaults to enabled when never called.
|
||||||
|
*/
|
||||||
|
EJOC_API int EJOC_CALL ejoc_renderer_set_dc_filter(ejoc_renderer_handle handle, uint32_t enabled);
|
||||||
EJOC_API uint32_t EJOC_CALL ejoc_renderer_thread_count(ejoc_renderer_handle handle);
|
EJOC_API uint32_t EJOC_CALL ejoc_renderer_thread_count(ejoc_renderer_handle handle);
|
||||||
EJOC_API const char* EJOC_CALL ejoc_renderer_last_error(ejoc_renderer_handle handle);
|
EJOC_API const char* EJOC_CALL ejoc_renderer_last_error(ejoc_renderer_handle handle);
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,10 @@ public:
|
|||||||
return error_[0] ? error_ : "";
|
return error_[0] ? error_ : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void set_dc_filter(const bool enabled) noexcept {
|
||||||
|
dc_filter_enabled_ = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
int process(
|
int process(
|
||||||
const float* bed5,
|
const float* bed5,
|
||||||
const float* lfe,
|
const float* lfe,
|
||||||
@@ -305,16 +309,18 @@ private:
|
|||||||
for (int i = 0; i < 4; ++i) {
|
for (int i = 0; i < 4; ++i) {
|
||||||
dc_buffer[20 + i] = current[i][0];
|
dc_buffer[20 + i] = current[i][0];
|
||||||
}
|
}
|
||||||
for (int slot = 0; slot < 4; ++slot) {
|
if (dc_filter_enabled_) {
|
||||||
Complex sum{0.0, 0.0};
|
for (int slot = 0; slot < 4; ++slot) {
|
||||||
for (int tap = 0; tap < 21; ++tap) {
|
Complex sum{0.0, 0.0};
|
||||||
const Complex sample = dc_buffer[slot + tap];
|
for (int tap = 0; tap < 21; ++tap) {
|
||||||
const double cr = kDcB[tap];
|
const Complex sample = dc_buffer[slot + tap];
|
||||||
const double ci = kDcA[tap];
|
const double cr = kDcB[tap];
|
||||||
sum.re += sample.re * cr - sample.im * ci;
|
const double ci = kDcA[tap];
|
||||||
sum.im += sample.re * ci + sample.im * cr;
|
sum.re += sample.re * cr - sample.im * ci;
|
||||||
|
sum.im += sample.re * ci + sample.im * cr;
|
||||||
|
}
|
||||||
|
x_[channel][0][group + slot] = {2.0 * sum.re, 2.0 * sum.im};
|
||||||
}
|
}
|
||||||
x_[channel][0][group + slot] = {2.0 * sum.re, 2.0 * sum.im};
|
|
||||||
}
|
}
|
||||||
for (int i = 0; i < 20; ++i) {
|
for (int i = 0; i < 20; ++i) {
|
||||||
surround_history_[surround][i] = dc_buffer[i + 4];
|
surround_history_[surround][i] = dc_buffer[i + 4];
|
||||||
@@ -641,6 +647,8 @@ private:
|
|||||||
float analysis_phase_;
|
float analysis_phase_;
|
||||||
alignas(64) Complex surround_delay_[2][10][64];
|
alignas(64) Complex surround_delay_[2][10][64];
|
||||||
alignas(64) Complex surround_history_[2][20];
|
alignas(64) Complex surround_history_[2][20];
|
||||||
|
// band-0 的 21-tap DC 补偿开关;仅 downmix 配置 3/4 由调用方置位。
|
||||||
|
bool dc_filter_enabled_ = true;
|
||||||
alignas(64) double lfe_delay_[kLfeDelay];
|
alignas(64) double lfe_delay_[kLfeDelay];
|
||||||
alignas(64) double matrix_previous_[15][5][64];
|
alignas(64) double matrix_previous_[15][5][64];
|
||||||
alignas(64) double synthesis_state_[15][640];
|
alignas(64) double synthesis_state_[15][640];
|
||||||
@@ -696,6 +704,14 @@ int EJOC_CALL ejoc_renderer_set_threads(ejoc_renderer_handle handle, uint32_t to
|
|||||||
return static_cast<ejoc::Renderer*>(handle)->set_threads(total_threads);
|
return static_cast<ejoc::Renderer*>(handle)->set_threads(total_threads);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int EJOC_CALL ejoc_renderer_set_dc_filter(ejoc_renderer_handle handle, uint32_t enabled) {
|
||||||
|
if (!handle) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
static_cast<ejoc::Renderer*>(handle)->set_dc_filter(enabled != 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t EJOC_CALL ejoc_renderer_thread_count(ejoc_renderer_handle handle) {
|
uint32_t EJOC_CALL ejoc_renderer_thread_count(ejoc_renderer_handle handle) {
|
||||||
if (!handle) {
|
if (!handle) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ import numpy as np
|
|||||||
|
|
||||||
from adm_atmos import q_to_adm_xyz
|
from adm_atmos import q_to_adm_xyz
|
||||||
from oamd_bits import JocFieldState, frame_update
|
from oamd_bits import JocFieldState, frame_update
|
||||||
|
from oamd_tracks import align_metadata_sample
|
||||||
from variant_error import UnsupportedVariantError
|
from variant_error import UnsupportedVariantError
|
||||||
|
|
||||||
OAMD_UPDATE_QUANTUM_SAMPLES = 64
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class PositionTransition:
|
class PositionTransition:
|
||||||
@@ -154,11 +153,8 @@ class OamdPositionTimeline:
|
|||||||
self.payload_count += 1
|
self.payload_count += 1
|
||||||
return
|
return
|
||||||
|
|
||||||
ramp_duration = int(update["ramp_duration_samples"])
|
effective_ramp = max(0, int(update["ramp_duration_samples"]))
|
||||||
effective_ramp = max(0, ramp_duration - OAMD_UPDATE_QUANTUM_SAMPLES)
|
transition_start = align_metadata_sample(coded_event + object_delay)
|
||||||
transition_start = coded_event + object_delay
|
|
||||||
if effective_ramp:
|
|
||||||
transition_start += OAMD_UPDATE_QUANTUM_SAMPLES
|
|
||||||
for index, target in enumerate(targets):
|
for index, target in enumerate(targets):
|
||||||
if self.previous_targets[index] == target:
|
if self.previous_targets[index] == target:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ def _load_huff_tables():
|
|||||||
H = _load_huff_tables()
|
H = _load_huff_tables()
|
||||||
|
|
||||||
JOC_NUM_CHANNELS = {0: 5, 1: 7, 2: 7, 3: 5, 4: 7} # Table 33
|
JOC_NUM_CHANNELS = {0: 5, 1: 7, 2: 7, 3: 5, 4: 7} # Table 33
|
||||||
|
# band-0 的 21-tap DC 补偿只在 downmix 配置 3/4 下启用。
|
||||||
|
DC_FILTER_DMX_CONFIGS = (3, 4)
|
||||||
JOC_NUM_BANDS = {0: 1, 1: 3, 2: 5, 3: 7, 4: 9, 5: 12, 6: 15, 7: 23} # Table 35
|
JOC_NUM_BANDS = {0: 1, 1: 3, 2: 5, 3: 7, 4: 9, 5: 12, 6: 15, 7: 23} # Table 35
|
||||||
JOC_NUM_QUANT = {0: 96, 1: 192} # Table 51
|
JOC_NUM_QUANT = {0: 96, 1: 192} # Table 51
|
||||||
# dense 的量化零点就是 nquant/2;sparse 的递推起点比它高 2 个量化步。
|
# dense 的量化零点就是 nquant/2;sparse 的递推起点比它高 2 个量化步。
|
||||||
|
|||||||
+9
-4
@@ -130,8 +130,12 @@ _SURROUND_DC_B = np.array([
|
|||||||
_SURROUND_DC_C = _SURROUND_DC_B + 1j * _SURROUND_DC_A
|
_SURROUND_DC_C = _SURROUND_DC_B + 1j * _SURROUND_DC_A
|
||||||
|
|
||||||
|
|
||||||
def surround_post_frame(x, delay, dc_hist):
|
def surround_post_frame(x, delay, dc_hist, apply_dc_filter=True):
|
||||||
"""处理 Ls/Rs 的 10 槽延迟、-j 旋转和 band-0 FIR。"""
|
"""处理 Ls/Rs 的 10 槽延迟、-j 旋转和 band-0 FIR。
|
||||||
|
|
||||||
|
``apply_dc_filter=False`` 时跳过 band-0 的 21-tap DC 补偿,只做延迟与
|
||||||
|
-j 旋转;延迟线与 DC 历史仍照常推进,便于逐帧切换。
|
||||||
|
"""
|
||||||
src = np.asarray(x, dtype=np.complex128)
|
src = np.asarray(x, dtype=np.complex128)
|
||||||
qdelay = np.asarray(delay, dtype=np.complex128).copy()
|
qdelay = np.asarray(delay, dtype=np.complex128).copy()
|
||||||
hist = np.asarray(dc_hist, dtype=np.complex128).copy()
|
hist = np.asarray(dc_hist, dtype=np.complex128).copy()
|
||||||
@@ -144,8 +148,9 @@ def surround_post_frame(x, delay, dc_hist):
|
|||||||
block = -1j * queued[:, :4, :]
|
block = -1j * queued[:, :4, :]
|
||||||
qdelay = queued[:, 4:, :]
|
qdelay = queued[:, 4:, :]
|
||||||
dc_buf = np.concatenate((hist, current[:, :, 0]), axis=1)
|
dc_buf = np.concatenate((hist, current[:, :, 0]), axis=1)
|
||||||
windows = np.lib.stride_tricks.sliding_window_view(dc_buf, 21, axis=1)
|
if apply_dc_filter:
|
||||||
block[:, :, 0] = 2.0 * np.sum(windows * _SURROUND_DC_C[None, None, :], axis=2)
|
windows = np.lib.stride_tricks.sliding_window_view(dc_buf, 21, axis=1)
|
||||||
|
block[:, :, 0] = 2.0 * np.sum(windows * _SURROUND_DC_C[None, None, :], axis=2)
|
||||||
hist = dc_buf[:, 4:]
|
hist = dc_buf[:, 4:]
|
||||||
out[:, :, group:group + 4] = block.transpose(0, 2, 1)
|
out[:, :, group:group + 4] = block.transpose(0, 2, 1)
|
||||||
return out, qdelay, hist
|
return out, qdelay, hist
|
||||||
|
|||||||
+20
-1
@@ -14,7 +14,7 @@ import sys
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from evo_unpack import unpack_evolution
|
from evo_unpack import unpack_evolution
|
||||||
from joc_decode import dequantize, diff_decode, parse_joc
|
from joc_decode import DC_FILTER_DMX_CONFIGS, dequantize, diff_decode, parse_joc
|
||||||
|
|
||||||
FRAME_SAMPLES = 1536
|
FRAME_SAMPLES = 1536
|
||||||
MAX_OBJECTS = 15
|
MAX_OBJECTS = 15
|
||||||
@@ -88,6 +88,9 @@ def _load_library(path):
|
|||||||
lib.ejoc_renderer_reset.restype = ctypes.c_int
|
lib.ejoc_renderer_reset.restype = ctypes.c_int
|
||||||
lib.ejoc_renderer_set_threads.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
|
lib.ejoc_renderer_set_threads.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
|
||||||
lib.ejoc_renderer_set_threads.restype = ctypes.c_int
|
lib.ejoc_renderer_set_threads.restype = ctypes.c_int
|
||||||
|
if hasattr(lib, "ejoc_renderer_set_dc_filter"):
|
||||||
|
lib.ejoc_renderer_set_dc_filter.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
|
||||||
|
lib.ejoc_renderer_set_dc_filter.restype = ctypes.c_int
|
||||||
lib.ejoc_renderer_thread_count.argtypes = [ctypes.c_void_p]
|
lib.ejoc_renderer_thread_count.argtypes = [ctypes.c_void_p]
|
||||||
lib.ejoc_renderer_thread_count.restype = ctypes.c_uint32
|
lib.ejoc_renderer_thread_count.restype = ctypes.c_uint32
|
||||||
lib.ejoc_renderer_last_error.argtypes = [ctypes.c_void_p]
|
lib.ejoc_renderer_last_error.argtypes = [ctypes.c_void_p]
|
||||||
@@ -225,6 +228,21 @@ class NativeJocRenderer:
|
|||||||
self._dq[object_index, :points, :, :bands] = values
|
self._dq[object_index, :points, :, :bands] = values
|
||||||
return mask
|
return mask
|
||||||
|
|
||||||
|
def _set_dc_filter(self, dmx_config_idx):
|
||||||
|
"""band-0 的 21-tap DC 补偿只在 downmix 配置 3/4 下启用。"""
|
||||||
|
enabled = dmx_config_idx in DC_FILTER_DMX_CONFIGS
|
||||||
|
setter = getattr(self._lib, "ejoc_renderer_set_dc_filter", None)
|
||||||
|
if setter is None:
|
||||||
|
if not enabled:
|
||||||
|
raise RuntimeError(
|
||||||
|
"native library has no ejoc_renderer_set_dc_filter; rebuild "
|
||||||
|
"eac3joc_core to disable the band-0 DC filter for "
|
||||||
|
f"dmx_config_idx={dmx_config_idx}")
|
||||||
|
return
|
||||||
|
result = setter(self._handle, 1 if enabled else 0)
|
||||||
|
if result != 0:
|
||||||
|
self._raise_native("set_dc_filter", result)
|
||||||
|
|
||||||
def render_frame(self, payload_bytes, bed5_pcm, lfe_pcm=None):
|
def render_frame(self, payload_bytes, bed5_pcm, lfe_pcm=None):
|
||||||
subs, _ = unpack_evolution(payload_bytes, loose=True)
|
subs, _ = unpack_evolution(payload_bytes, loose=True)
|
||||||
return self.render_subpayloads(subs, bed5_pcm, lfe_pcm)
|
return self.render_subpayloads(subs, bed5_pcm, lfe_pcm)
|
||||||
@@ -233,6 +251,7 @@ class NativeJocRenderer:
|
|||||||
self._require_open()
|
self._require_open()
|
||||||
out, _, mix_dq = self.decode_subpayloads(subs)
|
out, _, mix_dq = self.decode_subpayloads(subs)
|
||||||
object_mask = self._pack_frame(out, mix_dq)
|
object_mask = self._pack_frame(out, mix_dq)
|
||||||
|
self._set_dc_filter(out["dmx_config_idx"])
|
||||||
bed5 = np.ascontiguousarray(bed5_pcm, dtype=np.float32)
|
bed5 = np.ascontiguousarray(bed5_pcm, dtype=np.float32)
|
||||||
if bed5.shape != (CORE_CHANNELS, FRAME_SAMPLES):
|
if bed5.shape != (CORE_CHANNELS, FRAME_SAMPLES):
|
||||||
raise ValueError(f"core PCM shape must be (5,1536), got {bed5.shape}")
|
raise ValueError(f"core PCM shape must be (5,1536), got {bed5.shape}")
|
||||||
|
|||||||
+53
-39
@@ -5,6 +5,18 @@ from adm_atmos import q_to_adm_xyz
|
|||||||
from oamd_bits import JocFieldState, frame_update
|
from oamd_bits import JocFieldState, frame_update
|
||||||
from variant_error import UnsupportedVariantError
|
from variant_error import UnsupportedVariantError
|
||||||
|
|
||||||
|
# 元数据更新时刻按渲染器处理块对齐,与 Dolby 的 processing_block_size 及
|
||||||
|
# speaker_renderer 的 block_size 落在同一网格。
|
||||||
|
METADATA_BLOCK_SAMPLES = 32
|
||||||
|
|
||||||
|
|
||||||
|
def align_metadata_sample(sample, block_samples=METADATA_BLOCK_SAMPLES):
|
||||||
|
"""把更新时刻量化到处理块边界。"""
|
||||||
|
block = int(block_samples)
|
||||||
|
if block <= 0:
|
||||||
|
raise ValueError("block_samples must be positive")
|
||||||
|
return block * ((int(sample) + block // 2 - 1) // block)
|
||||||
|
|
||||||
|
|
||||||
def _lerp_xyz(start, target, amount):
|
def _lerp_xyz(start, target, amount):
|
||||||
return tuple(a + (b - a) * amount for a, b in zip(start, target))
|
return tuple(a + (b - a) * amount for a, b in zip(start, target))
|
||||||
@@ -18,18 +30,19 @@ def _append_point(points, sample, xyz, interpolation_samples):
|
|||||||
points.append(item)
|
points.append(item)
|
||||||
|
|
||||||
|
|
||||||
def _expand_events_dense64(events, total_samples, rate, update_quantum_samples,
|
def _expand_events_dense64(events, total_samples, rate, update_block_samples,
|
||||||
object_delay_samples, object_index):
|
object_delay_samples, object_index):
|
||||||
if not events:
|
if not events:
|
||||||
return [(0.0, 0.0, 0.0, 0.0, total_samples / float(rate), 0.0)]
|
return [(0.0, 0.0, 0.0, 0.0, total_samples / float(rate), 0.0)]
|
||||||
|
|
||||||
|
block = int(update_block_samples)
|
||||||
# 初始位置从成品 sample 0 起有效;合成延迟只作用于后续位置变化。
|
# 初始位置从成品 sample 0 起有效;合成延迟只作用于后续位置变化。
|
||||||
current = events[0][1]
|
current = events[0][1]
|
||||||
points = []
|
points = []
|
||||||
_append_point(points, 0, current, 0)
|
_append_point(points, 0, current, 0)
|
||||||
|
|
||||||
for event_index, (coded_start, target, ramp_samples) in enumerate(events[1:], 1):
|
for event_index, (coded_start, target, ramp_samples) in enumerate(events[1:], 1):
|
||||||
start = coded_start + object_delay_samples
|
start = align_metadata_sample(coded_start + object_delay_samples, block)
|
||||||
if start >= total_samples:
|
if start >= total_samples:
|
||||||
break
|
break
|
||||||
if start < points[-1][0]:
|
if start < points[-1][0]:
|
||||||
@@ -43,15 +56,16 @@ def _expand_events_dense64(events, total_samples, rate, update_quantum_samples,
|
|||||||
if start > points[-1][0]:
|
if start > points[-1][0]:
|
||||||
_append_point(points, start, current, 0)
|
_append_point(points, start, current, 0)
|
||||||
|
|
||||||
effective_ramp = max(0, int(ramp_samples) - update_quantum_samples)
|
ramp = max(0, int(ramp_samples))
|
||||||
if effective_ramp == 0:
|
if ramp == 0:
|
||||||
_append_point(points, start, target, 0)
|
_append_point(points, start, target, 0)
|
||||||
current = target
|
current = target
|
||||||
continue
|
continue
|
||||||
|
|
||||||
end = start + math.ceil(effective_ramp / update_quantum_samples) * update_quantum_samples
|
end = start + math.ceil(ramp / block) * block
|
||||||
if event_index + 1 < len(events):
|
if event_index + 1 < len(events):
|
||||||
next_start = events[event_index + 1][0] + object_delay_samples
|
next_start = align_metadata_sample(
|
||||||
|
events[event_index + 1][0] + object_delay_samples, block)
|
||||||
if next_start < end:
|
if next_start < end:
|
||||||
raise UnsupportedVariantError(
|
raise UnsupportedVariantError(
|
||||||
"oamd", "overlapping_position_ramps",
|
"oamd", "overlapping_position_ramps",
|
||||||
@@ -61,23 +75,23 @@ def _expand_events_dense64(events, total_samples, rate, update_quantum_samples,
|
|||||||
"ramp_start_sample": start,
|
"ramp_start_sample": start,
|
||||||
"ramp_end_sample": end,
|
"ramp_end_sample": end,
|
||||||
"next_update_sample": next_start,
|
"next_update_sample": next_start,
|
||||||
"repair_hint": "按 64-sample 状态机截断旧 ramp,再从当前插值位置启动新 ramp",
|
"repair_hint": "按 32-sample 状态机截断旧 ramp,再从当前插值位置启动新 ramp",
|
||||||
})
|
})
|
||||||
|
|
||||||
# 逐位置更新节拍复现状态机。1536-sample ramp 在首次 64-sample
|
# 从对齐后的更新点起按处理块推进,整条 ramp 覆盖 ramp_samples 个样本;
|
||||||
# 更新后剩余 1472 samples,因此共有 23 个中间/终点坐标。
|
# 几何式逼近望远镜化简为精确线性,每个中间点都落在真实 ramp 上。
|
||||||
future = effective_ramp
|
future = ramp
|
||||||
elapsed = 0
|
elapsed = 0
|
||||||
position = current
|
position = current
|
||||||
while future > 0:
|
while future > 0:
|
||||||
amount = min(update_quantum_samples / float(future), 1.0)
|
amount = min(block / float(future), 1.0)
|
||||||
position = _lerp_xyz(position, target, amount)
|
position = _lerp_xyz(position, target, amount)
|
||||||
elapsed += update_quantum_samples
|
elapsed += block
|
||||||
sample = start + elapsed
|
sample = start + elapsed
|
||||||
if sample >= total_samples:
|
if sample >= total_samples:
|
||||||
break
|
break
|
||||||
_append_point(points, sample, position, update_quantum_samples)
|
_append_point(points, sample, position, block)
|
||||||
future -= update_quantum_samples
|
future -= block
|
||||||
current = target
|
current = target
|
||||||
|
|
||||||
blocks = []
|
blocks = []
|
||||||
@@ -94,16 +108,14 @@ def _expand_events_dense64(events, total_samples, rate, update_quantum_samples,
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _compact_events(events, total_samples, rate, update_quantum_samples,
|
def _compact_events(events, total_samples, rate, update_block_samples,
|
||||||
object_delay_samples, object_index):
|
object_delay_samples, object_index):
|
||||||
"""Represent each linear OAMD ramp with one ADM interpolation block.
|
"""Represent each linear OAMD ramp with one ADM interpolation block.
|
||||||
|
|
||||||
The existing dense64 representation keeps the old position at ``start``,
|
The update instant is quantized to the processing block boundary, the motion
|
||||||
writes its first interpolated target at ``start + quantum``, and lets ADM
|
starts there immediately, and ``interpolationLength`` spans the full
|
||||||
interpolate that block over one quantum. Consequently, the interpreted
|
``ramp_duration``. The interpreted motion therefore covers
|
||||||
motion begins at ``start + quantum`` and reaches the final target at
|
``[align(start), align(start) + ramp_duration]``.
|
||||||
``start + ramp_duration``. This compact form preserves that timing with one
|
|
||||||
target block whose interpolationLength is ``ramp_duration - quantum``.
|
|
||||||
"""
|
"""
|
||||||
if not events:
|
if not events:
|
||||||
return [(0.0, 0.0, 0.0, 0.0, total_samples / float(rate), 0.0)]
|
return [(0.0, 0.0, 0.0, 0.0, total_samples / float(rate), 0.0)]
|
||||||
@@ -112,15 +124,16 @@ def _compact_events(events, total_samples, rate, update_quantum_samples,
|
|||||||
current = events[0][1]
|
current = events[0][1]
|
||||||
_append_point(points, 0, current, 0)
|
_append_point(points, 0, current, 0)
|
||||||
|
|
||||||
|
block = int(update_block_samples)
|
||||||
for event_index, (coded_start, target, ramp_samples) in enumerate(events[1:], 1):
|
for event_index, (coded_start, target, ramp_samples) in enumerate(events[1:], 1):
|
||||||
event_start = coded_start + object_delay_samples
|
event_start = align_metadata_sample(coded_start + object_delay_samples, block)
|
||||||
if event_start >= total_samples:
|
if event_start >= total_samples:
|
||||||
break
|
break
|
||||||
effective_ramp = max(0, int(ramp_samples) - update_quantum_samples)
|
ramp = max(0, int(ramp_samples))
|
||||||
block_start = event_start + (update_quantum_samples if effective_ramp else 0)
|
block_start = event_start
|
||||||
if block_start >= total_samples:
|
if block_start >= total_samples:
|
||||||
break
|
break
|
||||||
ramp_end = block_start + effective_ramp
|
ramp_end = block_start + ramp
|
||||||
|
|
||||||
if block_start < points[-1][0]:
|
if block_start < points[-1][0]:
|
||||||
raise UnsupportedVariantError(
|
raise UnsupportedVariantError(
|
||||||
@@ -131,9 +144,9 @@ def _compact_events(events, total_samples, rate, update_quantum_samples,
|
|||||||
|
|
||||||
if event_index + 1 < len(events):
|
if event_index + 1 < len(events):
|
||||||
next_coded_start, _, next_ramp_samples = events[event_index + 1]
|
next_coded_start, _, next_ramp_samples = events[event_index + 1]
|
||||||
next_event_start = next_coded_start + object_delay_samples
|
next_event_start = align_metadata_sample(
|
||||||
next_effective = max(0, int(next_ramp_samples) - update_quantum_samples)
|
next_coded_start + object_delay_samples, block)
|
||||||
next_block_start = next_event_start + (update_quantum_samples if next_effective else 0)
|
next_block_start = next_event_start
|
||||||
if next_block_start < ramp_end:
|
if next_block_start < ramp_end:
|
||||||
raise UnsupportedVariantError(
|
raise UnsupportedVariantError(
|
||||||
"oamd", "overlapping_compact_position_ramps",
|
"oamd", "overlapping_compact_position_ramps",
|
||||||
@@ -147,10 +160,10 @@ def _compact_events(events, total_samples, rate, update_quantum_samples,
|
|||||||
})
|
})
|
||||||
|
|
||||||
block_target = target
|
block_target = target
|
||||||
block_interpolation = effective_ramp
|
block_interpolation = ramp
|
||||||
available = total_samples - block_start
|
available = total_samples - block_start
|
||||||
if effective_ramp > available:
|
if ramp > available:
|
||||||
block_target = _lerp_xyz(current, target, available / float(effective_ramp))
|
block_target = _lerp_xyz(current, target, available / float(ramp))
|
||||||
block_interpolation = available
|
block_interpolation = available
|
||||||
_append_point(points, block_start, block_target, block_interpolation)
|
_append_point(points, block_start, block_target, block_interpolation)
|
||||||
current = target
|
current = target
|
||||||
@@ -168,25 +181,26 @@ def _compact_events(events, total_samples, rate, update_quantum_samples,
|
|||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
def _expand_events(events, total_samples, rate, update_quantum_samples,
|
def _expand_events(events, total_samples, rate, update_block_samples,
|
||||||
object_delay_samples, object_index, trajectory_mode):
|
object_delay_samples, object_index, trajectory_mode):
|
||||||
if trajectory_mode == "compact":
|
if trajectory_mode == "compact":
|
||||||
return _compact_events(events, total_samples, rate, update_quantum_samples,
|
return _compact_events(events, total_samples, rate, update_block_samples,
|
||||||
object_delay_samples, object_index)
|
object_delay_samples, object_index)
|
||||||
if trajectory_mode == "dense64":
|
if trajectory_mode == "dense64":
|
||||||
return _expand_events_dense64(events, total_samples, rate, update_quantum_samples,
|
return _expand_events_dense64(events, total_samples, rate, update_block_samples,
|
||||||
object_delay_samples, object_index)
|
object_delay_samples, object_index)
|
||||||
raise ValueError(f"未知 trajectory_mode: {trajectory_mode}")
|
raise ValueError(f"未知 trajectory_mode: {trajectory_mode}")
|
||||||
|
|
||||||
def build_adm_tracks(index, frames=None, rate=48000, frame_samples=1536,
|
def build_adm_tracks(index, frames=None, rate=48000, frame_samples=1536,
|
||||||
update_quantum_samples=64, object_delay_samples=1473,
|
update_block_samples=METADATA_BLOCK_SAMPLES,
|
||||||
trajectory_mode="compact"):
|
object_delay_samples=1473, trajectory_mode="compact"):
|
||||||
"""从统一 metadata index 构造 15 条 ADM 轨迹。
|
"""从统一 metadata index 构造 15 条 ADM 轨迹。
|
||||||
|
|
||||||
返回 ``[(name, [(rtime,x,y,z,duration,interpolation), ...]), ...]``。
|
返回 ``[(name, [(rtime,x,y,z,duration,interpolation), ...]), ...]``。
|
||||||
OAMD 的内外层 sample offset、block offset 和 ramp 均保留。
|
OAMD 的内外层 sample offset、block offset 和 ramp 均保留;更新时刻量化到
|
||||||
|
``update_block_samples`` 的块边界,ramp 覆盖完整的 ramp_duration。
|
||||||
``trajectory_mode="compact"`` 用一个长 ADM interpolation block 表示每条
|
``trajectory_mode="compact"`` 用一个长 ADM interpolation block 表示每条
|
||||||
线性 ramp;``dense64`` 保留逐 64-sample 展开作为兼容回退。
|
线性 ramp;``dense64`` 保留逐块展开作为兼容回退。
|
||||||
``object_delay_samples`` 将位置更新与对象 PCM 的 decoder 输出时刻对齐。
|
``object_delay_samples`` 将位置更新与对象 PCM 的 decoder 输出时刻对齐。
|
||||||
slot1..15 与对象 PCM ch1..15 一一对应。
|
slot1..15 与对象 PCM ch1..15 一一对应。
|
||||||
"""
|
"""
|
||||||
@@ -221,7 +235,7 @@ def build_adm_tracks(index, frames=None, rate=48000, frame_samples=1536,
|
|||||||
return [
|
return [
|
||||||
(f"JOC_Object_{obj}",
|
(f"JOC_Object_{obj}",
|
||||||
_expand_events(events[obj - 1], total_samples, rate,
|
_expand_events(events[obj - 1], total_samples, rate,
|
||||||
update_quantum_samples, object_delay_samples, obj,
|
update_block_samples, object_delay_samples, obj,
|
||||||
trajectory_mode))
|
trajectory_mode))
|
||||||
for obj in range(1, 16)
|
for obj in range(1, 16)
|
||||||
]
|
]
|
||||||
|
|||||||
+8
-4
@@ -13,7 +13,7 @@ output_scale:
|
|||||||
"""
|
"""
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from joc_decode import parse_joc, diff_decode, dequantize
|
from joc_decode import DC_FILTER_DMX_CONFIGS, parse_joc, diff_decode, dequantize
|
||||||
from joc_qmf import (N, QMF5_WINDOW, qmf_analysis_frame,
|
from joc_qmf import (N, QMF5_WINDOW, qmf_analysis_frame,
|
||||||
surround_post_frame, interp_matrix)
|
surround_post_frame, interp_matrix)
|
||||||
from evo_unpack import unpack_evolution
|
from evo_unpack import unpack_evolution
|
||||||
@@ -60,11 +60,12 @@ class JocRenderer:
|
|||||||
mix_dq = dequantize(out, mix_q)
|
mix_dq = dequantize(out, mix_q)
|
||||||
return out, mix_q, mix_dq
|
return out, mix_q, mix_dq
|
||||||
|
|
||||||
def qmf_x(self, bed5, phase_new=0.0625):
|
def qmf_x(self, bed5, phase_new=0.0625, apply_dc_filter=True):
|
||||||
"""核心 5ch PCM → 对象矩阵使用的复数 QMF ``x``。
|
"""核心 5ch PCM → 对象矩阵使用的复数 QMF ``x``。
|
||||||
|
|
||||||
先以 float32 对当前帧应用 phase;phase 变化时仅前 256 个样本从旧值
|
先以 float32 对当前帧应用 phase;phase 变化时仅前 256 个样本从旧值
|
||||||
线性过渡。缩放后的 L/R/C 延迟 10 槽,Ls/Rs 不延迟,再进入分析 QMF。
|
线性过渡。缩放后的 L/R/C 延迟 10 槽,Ls/Rs 不延迟,再进入分析 QMF。
|
||||||
|
``apply_dc_filter`` 控制 Ls/Rs band-0 的 21-tap DC 补偿。
|
||||||
"""
|
"""
|
||||||
pcm = np.asarray(bed5, dtype=np.float32)
|
pcm = np.asarray(bed5, dtype=np.float32)
|
||||||
if pcm.shape != (5, 1536):
|
if pcm.shape != (5, 1536):
|
||||||
@@ -87,7 +88,8 @@ class JocRenderer:
|
|||||||
x, self._analysis_fifo = qmf_analysis_frame(self._analysis_fifo, blocks)
|
x, self._analysis_fifo = qmf_analysis_frame(self._analysis_fifo, blocks)
|
||||||
self._analysis_phase = new_phase
|
self._analysis_phase = new_phase
|
||||||
x[3:], self._surround_qmf_delay, self._surround_dc_hist = surround_post_frame(
|
x[3:], self._surround_qmf_delay, self._surround_dc_hist = surround_post_frame(
|
||||||
x[3:], self._surround_qmf_delay, self._surround_dc_hist)
|
x[3:], self._surround_qmf_delay, self._surround_dc_hist,
|
||||||
|
apply_dc_filter=apply_dc_filter)
|
||||||
return x
|
return x
|
||||||
|
|
||||||
def object_z(self, out, mix_dq, x):
|
def object_z(self, out, mix_dq, x):
|
||||||
@@ -244,7 +246,9 @@ class JocRenderer:
|
|||||||
def render_subpayloads(self, subs, bed5_pcm, lfe_pcm=None):
|
def render_subpayloads(self, subs, bed5_pcm, lfe_pcm=None):
|
||||||
"""以已拆出的 EMDF payload 字典渲染一帧,避免绑定 transport 容器。"""
|
"""以已拆出的 EMDF payload 字典渲染一帧,避免绑定 transport 容器。"""
|
||||||
out, mix_q, mix_dq = self.decode_subpayloads(subs)
|
out, mix_q, mix_dq = self.decode_subpayloads(subs)
|
||||||
x = self.qmf_x(bed5_pcm)
|
x = self.qmf_x(
|
||||||
|
bed5_pcm,
|
||||||
|
apply_dc_filter=out["dmx_config_idx"] in DC_FILTER_DMX_CONFIGS)
|
||||||
self._last_x = x.copy()
|
self._last_x = x.copy()
|
||||||
z_all = self.object_z(out, mix_dq, x)
|
z_all = self.object_z(out, mix_dq, x)
|
||||||
# joc_clipgain 在对象逆 QMF 后应用,并与用户 output_scale 分离。
|
# joc_clipgain 在对象逆 QMF 后应用,并与用户 output_scale 分离。
|
||||||
|
|||||||
Reference in New Issue
Block a user