Sign in

    MoonSRC

    Pure MoonBit bounded streaming sample rate conversion with anti-alias FIR filtering.

    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    5 days ago
    Downloads
    14

    No dependencies

    #MoonSRC

    纯 MoonBit 的流式音频采样率转换库。使用带抗混叠低通的多相 Blackman sinc FIR, 支持交错 PCM、有限输出缓冲区和明确的 finish 语义。

    #支持范围

    维度功能
    比率44100↔48000、48000↔16000 Hz;16000/44100/48000 同速率直通
    声道1 或 2,交错排列
    样本有限 Double;核心不截幅,拒绝 NaN/Infinity
    核心后端native、JavaScript、wasm-gc,均有本机运行证据
    WAVRIFF/WAVE PCM16LE;native CLI;其他编码明确拒绝
    边界两端零延拓;非同速率有 96 输入帧前瞻

    最终输出帧数为 ceil(N * output_rate / input_rate)。帧数乘以声道数才是样本数。 同速率路径逐样本保持原值;库的浮点输出没有 PCM16 量化。

    #从源码验证

    需要 MoonBit、native C 编译器、Node.js 和 Python 3.10+。本机使用 Moon 0.1.20260920、moonc 0.10.14+7d59c7ec9;CI 固定对应官方工具链。 本项目无第三方 Mooncakes 依赖。源码目录下运行:

    moon fmt --check moon check --target native --deny-warn moon test --target native --deny-warn moon test --target js --deny-warn moon test --target wasm-gc --deny-warn python scripts/e2e.py python scripts/quality.py python scripts/run_examples.py python scripts/bounded_stream.py python scripts/consumer.py

    consumer.py 用 moon package 生成 ZIP,解压到全新工作区,通过临时 moon.work 让独立项目依赖本地包,并在三个后端运行。

    #库调用

    消费项目的 moon.pkg:

    ///|
    import {
    "Ag108/MoonSRC",
    }

    一次性处理复用同一个流式引擎:

    ///|
    test "readme/minimal stereo conversion" {
    let config = @MoonSRC.Config::new(44100, 48000, 2).unwrap()
    let input = Array::make(882, 0.25) // 441 stereo frames
    let output = @MoonSRC.resample(config, input[:]).unwrap()
    assert_eq(output.length(), 960) // 480 stereo frames
    }

    流式调用先创建 @MoonSRC.Resampler::new(config),用 process(input_view, output_buffer) 返回的 consumed_frames 推进输入,用 produced_frames 读取输出。 输入耗尽后反复 finish(output_buffer),直到 is_finished()。 output_buffer 是调用者提供的 FixedArray[Double],长度必须对齐完整声道帧。 可运行循环见 分块示例 和 消费项目验证程序。错误、背压与重置见 流式契约。

    #WAV CLI

    moon run --target native cmd/main -- --help moon run --target native cmd/main -- fixture _build/input.wav --rate 44100 --channels 2 --frames 441 moon run --target native cmd/main -- convert _build/input.wav _build/output.wav --rate 48000

    输出为 480 帧双声道 48 kHz PCM16 WAV,并报告输入/输出帧、前瞻和截幅数。 输出路径必须不存在;重复运行请换一个新路径。输入 WAV 最多 33,554,476 字节, 一次性转换最多 16,777,216 输出样本。长音频请使用分块库 API。 PCM16 写入显式饱和并采用离零舍入,详见 范围与量化。

    #五个示例

    moon run --target native examples/memory_44100_to_48000 moon run --target native examples/speech_48000_to_16000 moon run --target native examples/chunked_equivalence moon run --target native examples/channel_isolation moon run --target native examples/wav_convert -- _build/example_input.wav _build/example_output.wav

    示例分别观察 4410→4800 帧、通带/混叠测量、三种分块结果一致、右声道严格静音、 以及实际 WAV 文件的 441→480 帧转换。语音示例使用可复现的合成语音频带信号, 不包含私人录音。python scripts/run_examples.py 自动使用新的临时路径验证全部示例。

    #质量与资源实测

    48→16 kHz、0.5 幅度、1 秒正弦,裁去两端各 256 输出帧后,独立 Python 分析得到: 1 kHz 通带误差约 0.00000125 dB;12 kHz 输入在输出中折叠到 4 kHz 的分量, 比通带低约 110.61 dB。这是这组固定信号的结果,不代表全频段阻带保证。 见 质量方法和原始数据。

    48→16 kHz 单声道 1 分钟与 10 分钟的固定采样存储均为 393 个 Double, 历史峰值均为 193 帧。过程对象可能分配,未承诺实时零分配。 设备、原始时间、RSS 与局限见 基准。

    算法说明:ALGORITHM;测试:TESTING; 输入边界:SECURITY;来源:THIRD_PARTY; AI 辅助说明:AI_USAGE。许可为 Apache-2.0,见 LICENSE。

    Config

    pub struct Config {
    // private fields
    } derive(Eq,
    Debug
    )

    Validated immutable configuration for one interleaved PCM stream.

    Config::channels

    fn Config::channels(self : Config) -> Int

    Number of interleaved samples in each frame.

    Config::equal

    fn Config::equal(Config, Config) -> Bool

    Config::input_rate

    fn Config::input_rate(self : Config) -> Int

    Input sample rate, in complete frames per second.

    Config::new

    fn Config::new(input_rate : Int, output_rate : Int, channels : Int) -> Result[Config, ResampleError]

    Accept the 0.1.0 rate matrix and one or two channels. No allocation occurs.

    Config::not_equal

    fn Config::not_equal(x : Config, y : Config) -> Bool

    Config::output_frames

    fn Config::output_frames(self : Config, input_frames : Int64) -> Result[Int64, ResampleError]

    Final frame count: ceil(input_frames * output_rate / input_rate). Uses quotient/remainder decomposition so even valid huge downsampling counts do not overflow an intermediate product. Negative or unrepresentable counts return FrameOverflow.

    Config::output_rate

    fn Config::output_rate(self : Config) -> Int

    Output sample rate, in complete frames per second.

    Config::to_repr

    Latency

    pub(all) struct Latency {
    input_frames : Int
    output_frames : Int
    seconds : Double
    }

    Future input needed by the centered filter; no frames are prepended to output.

    Progress

    pub(all) struct Progress {
    consumed_frames : Int
    produced_frames : Int
    needs_input : Bool
    needs_output : Bool
    finished : Bool
    }

    Counts for this call, in complete frames. Retry unconsumed input.

    ResampleError

    pub(all) enum ResampleError {
    InvalidRate(Int)
    UnsupportedRatio(Int, Int)
    InvalidChannels(Int)
    MisalignedInput
    MisalignedOutput
    NonFinite(Int)
    InvalidState
    FrameOverflow
    NumericOverflow
    ResourceLimit
    } derive(Eq,
    Debug
    )

    Errors returned for invalid configuration, samples, resources or stream state.

    ResampleError::equal

    ResampleError::not_equal

    fn ResampleError::not_equal(x : ResampleError, y : ResampleError) -> Bool

    Resampler

    pub struct Resampler {
    // private fields
    }

    A single PCM stream. Internal sample storage has a fixed upper bound.

    Resampler::finish

    fn Resampler::finish(self : Resampler, output : FixedArray[Double]) -> Result[Progress, ResampleError]

    Signal end of input and drain the same FIR using zero extension. Repeated calls are allowed, including zero capacity; completed finish is idempotent.

    Resampler::is_finished

    fn Resampler::is_finished(self : Resampler) -> Bool

    True only when finish has drained all timestamped output frames.

    Resampler::latency

    fn Resampler::latency(self : Resampler) -> Latency

    Lookahead in input/output frames, and seconds. Identity has zero latency.

    Resampler::new

    fn Resampler::new(config : Config) -> Resampler

    Construct a bounded filter and ring. Config can only be created by validation.

    Resampler::process

    fn Resampler::process(self : Resampler, input : ArrayView[Double], output : FixedArray[Double]) -> Result[Progress, ResampleError]

    Validate the entire supplied view, then consume only reported frames. Zero output capacity consumes nothing. Invalid/misaligned/nonfinite inputs leave stream state unchanged. NumericOverflow requires reset before reuse.

    Resampler::reset

    fn Resampler::reset(self : Resampler) -> Unit

    Explicitly start an unrelated stream while reusing all allocated tables/ring.

    Resampler::stats

    fn Resampler::stats(self : Resampler) -> Stats

    Storage counts exclude caller buffers, object headers and allocator overhead.

    Stats

    pub(all) struct Stats {
    input_frames : Int64
    output_frames : Int64
    buffered_frames : Int
    peak_history_frames : Int
    history_capacity_samples : Int
    coefficient_samples : Int
    internal_samples : Int
    }

    Current stream counters and fixed storage budgets (Double slots, not RSS).

    StreamState

    type StreamState

    resample

    fn resample(config : Config, input : ArrayView[Double]) -> Result[Array[Double], ResampleError]

    Convert one finite interleaved buffer through the streaming engine. The convenience API limits output to 16,777,216 samples (128 MiB payload). Use Resampler with caller-owned blocks for longer audio.