XiStudio · DSP 算法架构设计 v1.0
设计定位
本文档描述 DSP 算法库(dsp_algo)在 Source/Sink Phase 8 改造中的架构定位。涵盖从"一锅炖 source_gen_v1"向"10 个独立模块"的演进、参数设计哲学、共享基础设施、与实时处理约束。
1. DSP 的核心职责
| 职责 | 负责模块 | 关键决策 |
|---|---|---|
| 信号生成 | source_sine_v1 等 5 个 tone 模块 |
相位累加、频率精度 |
| 噪声生成 | source_noise_white/pink/brown_v1 |
分布、谱形状、相关性 |
| 文件回放 | source_wav_v1 |
内存预读、采样率转换、seek |
| 设备采集 | source_device_v1 |
passthrough、外部注入接口 |
| 混音处理 | mixer_v2 |
多路求和、端口动态扩展 |
| 输出缓冲 | sink_v1_dsp |
环形缓冲、参数读取 |
| 链路执行 | DynamicChain_Process() |
拓扑执行、block 调度 |
2. 架构目标 vs 当前实现
2.1 模块拆分:从单体到独立职责
架构目标(Phase 8)
旧版 source_module_gen.c(280 行)中 10 个 case 高度耦合,参数名重叠、条件分支复杂:
// 旧做法
switch (param_id) {
case PARAM_GEN_SINE_FREQ:
case PARAM_GEN_NOISE_TYPE: // 噪声模块也用这个?混乱
case PARAM_GEN_WAV_PATH:
// ... 10 个 case 混在一起
}
新架构 10 个独立模块,每个模块只做一件事:
source_sine_v1/source_sweep_v1/source_triangle_v1/source_square_v1/source_saw_v1(5 个 tone)source_noise_white_v1/source_noise_pink_v1/source_noise_brown_v1(3 个噪声)source_wav_v1(文件回放)source_device_v1(设备采集)
当前实现(DSP 已准备就绪)
查看 dsp_algo/modules/source/ 目录:
source_sine.c / source_sine.h
source_sweep.c / source_sweep.h
source_triangle.c / source_triangle.h
source_square.c / source_square.h
source_saw.c / source_saw.h
source_noise_white.c / source_noise_white.h
source_noise_pink.c / source_noise_pink.h
source_noise_brown.c / source_noise_brown.h
source_wav.c / source_wav.h
source_device.c / source_device.h
source_common.c / source_common.h ← 共享基础设施
所有 10 个模块 + source_common 已完全实现,无需新建。阶段 2(DSP 拆分)主要是注册、打包、测试。
2.2 实时处理约束:零独立线程
架构目标(Q3 决定)
整个 DSP 处理链路由单一 Process 线程驱动,通常在 WASAPI callback 线程内执行:
- 不允许内部独立线程:所有 I/O / 参数更新都在 Process block 边界完成
- 不允许阻塞等待:所有操作必须在 1 block 内完成(@ 48 kHz 64 sample = 1.33 ms)
- 参数更新通过 SetParam 队列:后端通过无锁 SPSC queue 推入参数变更
当前实现
各模块都遵循 RT 约束设计:
| 模块 | I/O 风险 | 缓解策略 |
|---|---|---|
source_sine_v1 等 tone |
无 | 纯计算 |
source_noise_*_v1 |
无 | 纯计算 + 伪随机 |
source_wav_v1 |
高(磁盘读取) | 内存预读整段文件,seek 不做 I/O |
source_device_v1 |
高(WASAPI capture) | 后端独立 capture 线程 + SPSC queue 注入 |
sink_v1_dsp |
低 | 环形缓冲,外部消费 |
2.3 参数设计哲学:业务属性驱动
架构目标
参数不是随意定义的,而是按业务层信号属性来设计:
| 参数类型 | 职责 | 示例 |
|---|---|---|
| system | DSP 初始化必需,变更需要 SetLink | source_wav_v1.filePath / source_device_v1.deviceId |
| tunable | 运行中可变更,通过 SetParam | frequencyHz / amplitudeDb / phase |
| readonly | DSP 填回前端,前端只读 | source_wav_v1.durationSec / memoryMB |
| structural | 变更会改变 DSP 拓扑(稀有) | (通常无,见 Q7 讨论) |
当前实现
所有 10 个模块参数已按此分类设计。例如 source_wav_v1:
// system 类参数(SetLink 触发)
filePath: string
// tunable 类参数(SetParam 应用)
loop: bool
startOffsetSec: float
gainDb: float
readPosSec: float // 支持拖动进度条
// readonly 类参数(DSP 填回)
durationSec: float
sampleRate: int
numChannels: int
memoryMB: float
3. DSP 模块详解
3.1 Tone 类模块(5 个)
共享机制:PhaseAccumulator
所有 tone 模块共享同一个相位累加器核心:
// 共享 API(source_common.h)
float SourceCommon_PhaseAccumulator_Tick(
float frequency_hz,
float phase_per_sample,
float* current_phase_ptr,
int sample_rate
);
// 输出形状由各模块定制
// source_sine_v1: sin(phase)
// source_triangle_v1: triangle_wave(phase)
// source_square_v1: square_wave(phase, duty_cycle)
// source_saw_v1: sawtooth_wave(phase, direction)
// source_sweep_v1: sweep with variable freq
参数设计
| 模块 | 独特参数 |
|---|---|
sine |
phaseDeg / channelPhaseOffsetDeg |
sweep |
startFrequencyHz / endFrequencyHz / durationSec / sweepType / mode |
triangle |
symmetry (0.5=等腰,<0.5=左倾) |
square |
dutyCycle (占空比) |
saw |
direction (上升/下降) |
通道模式(所有 tone 共享)
// 所有 tone 都支持
outputChannels: int
channelPhaseOffsetDeg: float // 各通道相位差(立体声测试)
// 应用方式
for (ch = 0; ch < outputChannels; ch++) {
phase_offset = channelPhaseOffsetDeg * (ch / outputChannels);
output[ch] = tone_func(phase + phase_offset) * gain_lin;
}
3.2 噪声类模块(3 个)
信号属性驱动参数设计
| 噪声类型 | 谱形状 | 参数集 |
|---|---|---|
white |
平坦(0 dB/oct) | 基础:distribution / channelCorrelation / seed |
pink |
-3 dB/oct | + algorithm (Voss-McCartney / FIR 滤波网络) |
brown |
-6 dB/oct | + integratorLeakage (DC 防漂移) |
共享随机数状态
// source_common.h
typedef struct {
uint32_t state[4]; // xorshift128+ 状态
} SourceCommon_RandState_t;
// 所有噪声模块共享此结构,避免重复实现
float SourceCommon_GaussianFromUniform(SourceCommon_RandState_t* state);
float SourceCommon_NextRandom01(SourceCommon_RandState_t* state);
通道相关性模式
// 所有噪声都支持
channelCorrelation: enum { correlated, uncorrelated }
if (uncorrelated) {
// 每通道独立随机序列
for (ch = 0; ch < numCh; ch++) {
rand_state[ch] = advance_rng(rand_state[ch]);
output[ch] = noise_gen(rand_state[ch]);
}
} else {
// 所有通道同源(演讲厅声学测试)
rand_state = advance_rng(rand_state);
sample = noise_gen(rand_state);
for (ch = 0; ch < numCh; ch++) output[ch] = sample;
}
3.3 文件回放模块:source_wav_v1
设计原则:内存预读 + 原子 seek
Why 内存预读?
- Process 线程是 RT 线程(64 sample @ 48 kHz = 1.33 ms 预算),任何磁盘 I/O 都会 underrun
- 要支持拖动进度条(Q3 明确的 AWE 体验),seek 后需要立即响应,不能重新解码
- WAV 通常 <100 MB,内存成本可控
实现骨架:
typedef struct {
// 运行时只读(由加载阶段初始化)
float* samples; // 预解码 PCM(浮点)
uint64_t totalFrames;
uint32_t wav_sample_rate;
uint32_t num_channels;
// 运行时可变(SetParam 线程写 / Process 线程原子读)
_Atomic uint64_t readPos;
_Atomic float gainLin;
_Atomic int loop;
_Atomic int enable;
// 加载状态
int loaded;
char filePath[260];
} wav_module_state_t;
加载阶段(可在非 RT 线程)
// Phase 8 SetLink 触发,后端通过 SetParam 推入 filePath 时
SetParam(PARAM_WAV_FILE_PATH, "/path/to/file.wav")
├─ 触发 DSP 侧加载动作
├─ 打开 WAV 文件 → 读取 header(格式、采样率、通道数、帧数)
├─ 采样率转换(如 WAV 为 44.1 kHz,系统 48 kHz)
├─ 整段 PCM 解码 → malloc(totalFrames * numCh * sizeof(float))
├─ 填回只读参数(durationSec / sampleRate / numChannels / memoryMB)
└─ 广播 param_bulk_update 消息到前端
运行阶段(Process 线程)
void source_wav_process(wav_module_state_t* st, float* out[], int blockSize) {
if (!st->loaded || !st->enable) return;
uint64_t readPos = atomic_load(&st->readPos);
float gainLin = atomic_load(&st->gainLin);
int loop = atomic_load(&st->loop);
for (s = 0; s < blockSize; s++) {
float sample = st->samples[readPos * st->num_channels]; // 读取第一通道
// 应用增益
out[0][s] = sample * gainLin;
// 移进度
readPos++;
if (readPos >= st->totalFrames) {
if (loop) readPos = 0;
else { st->enable = false; break; }
}
}
atomic_store(&st->readPos, readPos);
}
Seek / 拖动进度条
SetParam(PARAM_WAV_READ_POS_SEC, 5.0) // 跳到 5.0 秒
├─ 前端计算 frame = 5.0 * wav_sample_rate
└─ 后端原子更新 readPos(无 I/O,仅改指针)
3.4 设备采集模块:source_device_v1
设计原则:passthrough + 后端 capture 线程
Why 不在 DSP 侧采集?
- WASAPI capture 需要持续 polling(或 callback),是后端音频驱动的责任
- DSP 侧应该完全独立于特定音频 API(可复用于 DSP-on-chip / SoC)
协议:
后端 capture 线程 DSP Process 线程
├─ WASAPI capture() → PCM
├─ 写入 capture_ringbuffer
└─ (定时,独立调度)
├─ 从 capture_ringbuffer 读取
├─ 应用 gainDb
└─ 输出到 mixer
DSP 侧实现(source_device_v1.c):
typedef struct {
float* input_buffer; // 后端写入的采样缓冲
uint32_t num_channels;
uint32_t buffer_size;
// 参数
_Atomic int enable;
_Atomic float gainLin;
_Atomic int input_channels_mask; // 从哪几个 channel 读?
} device_module_state_t;
void source_device_process(device_module_state_t* st, float* out[], int blockSize) {
if (!st->enable) return;
// Passthrough:直接复制后端注入的样本
for (s = 0; s < blockSize; s++) {
for (ch = 0; ch < st->num_channels; ch++) {
out[ch][s] = st->input_buffer[s * st->num_channels + ch] * st->gainLin;
}
}
}
3.5 设备输出模块:sink_v1_dsp
环形缓冲 + 参数读取
typedef struct {
float* ringbuffer;
uint64_t writePos;
uint64_t readPos; // 由外部(后端 render callback)推进
uint32_t capacity; // 通常 = 块数 * 块大小 * 2(防 underrun)
} sink_module_state_t;
void sink_v1_process(sink_module_state_t* st, float* in[], int blockSize) {
// 写入环形缓冲(写指针 DSP 管理)
for (s = 0; s < blockSize; s++) {
uint64_t idx = (st->writePos + s) % st->capacity;
st->ringbuffer[idx] = in[0][s];
}
st->writePos += blockSize;
}
// 后端定时读取
float* GetSinkBuffer(float** readBuffer, int* availableSamples) {
*readBuffer = st->ringbuffer + (st->readPos % st->capacity);
*availableSamples = st->writePos - st->readPos;
return *readBuffer;
}
// 消费完毕后推进读指针
void AdvanceSinkReadPos(int samplesConsumed) {
st->readPos += samplesConsumed;
}
4. 共享基础设施(source_common.h/c)
所有 source 模块共享的 API 与数据结构:
| API | 用途 |
|---|---|
SourceCommon_DbToLinear(db) |
dB → 线性增益转换 |
SourceCommon_PhaseAccumulator_Tick() |
相位累加(sine/triangle/square/saw 共用) |
SourceCommon_FillChannels() |
单通道信号复制到多通道 + 相位偏移 |
SourceCommon_GaussianFromUniform() |
Box-Muller 变换 |
SourceCommon_NextRandom01() |
伪随机数生成 |
SourceCommon_ParamRamp_Linear() |
参数平滑(线性斜升) |
SourceCommon_ParamRamp_IIR() |
参数平滑(IIR 低通) |
5. 注册与 Manifest 系统
5.1 Module Registry
dsp_algo/framework/module_registry_all.c 记录所有 10 个模块的元数据:
const ModuleDefinition module_registry[] = {
{
.typeId = 0x10080010,
.name = "source_sine_v1",
.init = source_sine_init,
.process = source_sine_process,
.setParam = source_sine_setParam,
.paramCount = 6,
.params = {
{ .id = 0, .name = "frequencyHz", .type = PARAM_FLOAT },
// ...
}
},
// ... 10 个模块
};
5.2 Module Manifest(Q1 之后引入)
Manifest 是高级元数据,包含:
- 参数生命周期标记(system / tunable / readonly / structural)
- 端口配置(input / output 端口数、通道、数据类型)
- UI 提示(range / unit / step size)
// 新增:include/module_manifest.h
typedef struct {
uint32_t typeId;
char* moduleName;
// 参数元数据数组
struct {
char* name;
int role; // PARAM_ROLE_SYSTEM / TUNABLE / READONLY / STRUCTURAL
float min, max, step;
char* unit; // "Hz", "dB", "sec", etc.
} paramManifests[MAX_PARAMS];
// 端口配置
struct {
int inputPortCount;
int outputPortCount;
char* signalType; // "float32", "int24", etc.
} portManifest;
} ModuleManifest_t;
6. Phase 8 DSP 改动清单
| 文件 | 改动 | 优先级 |
|---|---|---|
modules/source/source_sine.c ~ source_saw.c |
新建 5 个 tone 模块 | P0 |
modules/source/source_noise_white.c ~ source_brown.c |
新建 3 个噪声模块 | P0 |
modules/source/source_wav.c |
新建,WAV 文件回放 + 内存预读 | P0 |
modules/source/source_device.c |
新建,设备采集 passthrough | P0 |
modules/source/source_common.c/h |
新建,共享 API | P0 |
modules/source/source_module.c |
重命名/删除(v7.0 不兼容决定) | P0 |
modules/source/source_module_gen.c |
删除(v7.0 不兼容决定) | P0 |
framework/module_registry_all.c |
增加 10 个模块注册 + manifest | P1 |
include/module_type_id.h |
分配 typeId 0x10080010-0x10080019 | P0 |
include/module_manifest.h |
新建,高级元数据定义 | P1 |
7. 当前实现状态汇总
| 方面 | 状态 | 文件位置 |
|---|---|---|
| 10 个独立模块代码 | ✅ 已完成 | modules/source/*.c/h |
| 共享基础设施 | ✅ 已完成 | modules/source/source_common.c/h |
| module_registry 注册 | 🔄 计划中 | framework/module_registry_all.c |
| module_manifest 系统 | 🔄 计划中 | include/module_manifest.h |
| TypeId 分配 | ✅ 已完成 | include/module_type_id.h |
| 单元测试(≥3 个/模块) | 🔄 计划中 | tests/ |
8. 设计决策要点总结
8.1 为什么每个 tone 是独立模块?
旧做法(source_gen_v1 单体):参数耦合高,参数有效性条件复杂("如果 waveform=sine,则 symmetry 无意义")。
新做法(独立模块):每个模块参数清晰,条件分支最少,易于测试、复用、组合。
8.2 为什么 WAV 要内存预读?
RT 约束:Process 线程预算 1.33 ms,任何阻塞等待都导致 underrun。磁盘 I/O(通常 10-100 ms)不可接受。
拖动进度条:需要 O(1) seek,不能重新解码,所以必须预读整段。
8.3 为什么设备采集独立于 DSP?
职责分离:DSP 只做处理,I/O 由后端管理。DSP 侧模块对音频 API 无感知,可复用于其他场景。
线程模型:后端 capture 线程(WASAPI callback)与 DSP Process 线程通过无锁 queue 解耦。
9. 结论
DSP 算法库已经为 Phase 8 做好充分准备:
- 10 个独立模块 完全实现,代码质量高
- 共享基础设施 完整,避免重复
- 参数设计 按业务属性对标 AWE / VeriStudio 等行业标杆
- RT 约束 完全遵守,无内部独立线程
剩余工作聚焦于注册、测试、文档完善,不涉及算法核心改动。