跳转至
DRAFT

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 做好充分准备:

  1. 10 个独立模块 完全实现,代码质量高
  2. 共享基础设施 完整,避免重复
  3. 参数设计 按业务属性对标 AWE / VeriStudio 等行业标杆
  4. RT 约束 完全遵守,无内部独立线程

剩余工作聚焦于注册、测试、文档完善,不涉及算法核心改动。