【问题标题】:Efficiently generate a Sine wave in IOS在 IOS 中高效生成正弦波
【发布时间】:2023-03-22 07:33:02
【问题描述】:

为运行 IOS 的设备生成正弦波的最有效方法是什么。出于练习的目的,假设频率为 440Hz,采样率为 44100Hz,样本数为 1024。

一个普通的 C 实现看起来像。

#define SAMPLES 1024
#define TWO_PI (3.14159 * 2)
#define FREQUENCY 440
#define SAMPLING_RATE 44100

int main(int argc, const char * argv[]) {
    float samples[SAMPLES];

    float phaseIncrement = TWO_PI * FREQUENCY / SAMPLING_RATE;
    float currentPhase = 0.0;
    for (int i = 0; i < SAMPLES; i ++){
        samples[i] = sin(currentPhase);
        currentPhase += phaseIncrement;
    }

    return 0;
}

为了利用 Accelerate Framework 和 vecLib vvsinf 函数,可以将循环更改为仅执行加法。

#define SAMPLES 1024
#define TWO_PI (3.14159 * 2)
#define FREQUENCY 440
#define SAMPLING_RATE 44100

int main(int argc, const char * argv[]) {
    float samples[SAMPLES] __attribute__ ((aligned));
    float results[SAMPLES] __attribute__ ((aligned));

    float phaseIncrement = TWO_PI * FREQUENCY / SAMPLING_RATE;
    float currentPhase = 0.0;
    for (int i = 0; i < SAMPLES; i ++){
        samples[i] = currentPhase;
        currentPhase += phaseIncrement;
    }
    vvsinf(results, samples, SAMPLES);

    return 0;
}

但是就效率而言,我是否应该尽可能地应用 vvsinf 函数?

我不太了解 Accelerate 框架,不知道我是否也可以替换循环。有没有可以使用的 vecLib 或 vDSP 函数?

就此而言,是否可以使用完全不同的算法来用正弦波填充缓冲区?

【问题讨论】:

    标签: signal-processing vdsp


    【解决方案1】:

    鉴于您正在计算以固定增量增加的相位参数的正弦,使用this "How to Create Oscillators in Software" post 中描述的递归方程和this "DSP Trick: Sinusoidal Tone Generator" post 中描述的更多信息通常更快地实现信号生成,两者都在dspguru:

    y[n] = 2*cos(w)*y[n-1] - y[n-2]
    

    请注意,此递归方程可能会受到数值舍入误差累积的影响,您应避免一次计算太多样本(您选择SAMPLES == 1024 应该没问题)。在获得前两个值y[0]y[1](初始条件)后,可以使用此递推方程。由于您生成的初始阶段为 0,因此它们很简单:

    samples[0] = 0;
    samples[1] = sin(phaseIncrement); 
    

    或更一般地说,具有任意初始阶段(对于经常重新初始化递推方程以避免我前面提到的数值舍入误差累积特别有用):

    samples[0] = sin(initialPhase);
    samples[1] = sin(initialPhase+phaseIncrement); 
    

    然后可以直接用以下方法实现递归方程:

    float scale = 2*cos(phaseIncrement);
    // initialize first 2 samples for the 0 initial phase case
    samples[0] = 0;
    samples[1] = sin(phaseIncrement);     
    for (int i = 2; i < SAMPLES; i ++){
        samples[i] = scale * samples[i-1] - samples[i-2];
    }
    

    请注意,此实现可以通过计算具有适当相对相移的多个音调(每个音调具有相同的频率,但样本之间的相位增量较大)进行矢量化,然后将结果交错以获得原始音(例如计算sin(4*w*n)sin(4*w*n+w)sin(4*w*n+2*w)sin(4*w*n+3*w))。然而,这会使实现变得更加晦涩难懂,收益相对较小。

    或者,可以通过使用vDsp_deq22来实现等式:

    // setup dummy array which will hold zeros as input
    float nullInput[SAMPLES];
    memset(nullInput, 0, SAMPLES * sizeof(float));
    
    // setup filter coefficients
    float coefficients[5];
    coefficients[0] = 0;
    coefficients[1] = 0;
    coefficients[2] = 0;
    coefficients[3] = -2*cos(phaseIncrement);
    coefficients[4] = 1.0;
    
    // initialize first 2 samples for the 0 initial phase case
    samples[0] = 0;
    samples[1] = sin(phaseIncrement); 
    vDsp_deq22(nullInput, 1, coefficients, samples, 1, SAMPLES-2);
    

    【讨论】:

    • 感谢您的回答。我看过各种代码示例,我认为,通过在循环中包含某种检查来重置每个 TWO_PI 样本。然而,由于我们的目标是提高效率,一次生成 (N * TWO_PI * FREQUENCY / SAMPLING_RATE) 个样本是否可行。并重复直到缓冲区填满所需的样本总数?
    • 每 2pi 重置一次在基于查找表的实现中很常见(以保持表内的索引),甚至在直接依赖相位以减少具有非常大相位角的浮点不准确性的实现中也是常见的。由于这篇文章没有直接使用循环中的阶段,所以你没有这个问题。但是,您确实有错误累积(正如我在帖子中提到的),这确实可以通过在块中生成样本来解决(尽管块的大小不必与 NTWO_PIFREQUENCY /SAMPLING_RATE) 并重复直到缓冲区被填满。所以,是的。
    【解决方案2】:

    如果需要效率,您可以预加载 440hz (44100 / 440) 正弦波形查找表并在其周围循环,无需进一步映射或预加载 1hz (44100 / 44100) 正弦波形查找表并通过跳过样本来循环以达到 440hz,就像您通过增加相位计数器所做的那样。使用查找表应该比计算 sin() 更快。

    方法A(使用440hz正弦波):

    #define SAMPLES 1024
    #define FREQUENCY 440
    #define SAMPLING_RATE 44100
    #define WAVEFORM_LENGTH (SAMPLING / FREQUENCY)
    
    int main(int argc, const char * argv[]) {
        float waveform[WAVEFORM_LENGTH];
        LoadSinWaveForm(waveform);
    
        float samples[SAMPLES] __attribute__ ((aligned));
        float results[SAMPLES] __attribute__ ((aligned));
    
        for (int i = 0; i < SAMPLES; i ++){
            samples[i] = waveform[i % WAVEFORM_LENGTH];
        }
    
        vvsinf(results, samples, SAMPLES);
    
        return 0;
    }
    

    方法 B(使用 1hz 正弦波):

    #define SAMPLES 1024
    #define FREQUENCY 440
    #define TWO_PI (3.14159 * 2)
    #define SAMPLING_RATE 44100
    #define WAVEFORM_LENGTH SAMPLING_RATE // since it's 1hz
    
    int main(int argc, const char * argv[]) {
        float waveform[WAVEFORM_LENGTH];
        LoadSinWaveForm(waveform);
    
        float samples[SAMPLES] __attribute__ ((aligned));
        float results[SAMPLES] __attribute__ ((aligned));
    
        float phaseIncrement = TWO_PI * FREQUENCY / SAMPLING_RATE;
        float currentPhase = 0.0;
        for (int i = 0; i < SAMPLES; i ++){
            samples[i] = waveform[floor(currentPhase) % WAVEFORM_LENGTH];
            currentPhase += phaseIncrement;
        }
    
        vvsinf(results, samples, SAMPLES);
    
        return 0;
    }
    

    请注意:

    • 方法 A 容易受到频率不准确的影响,因为假设您的频率始终正确划分采样率,这是不正确的。这意味着您可能会得到 441hz 或 440hz 的故障。

    • 方法 B 在频率上升并接近奈奎斯特频率时容易出现混叠,但如果合成​​合理的低频(例如您在例子。

    【讨论】:

    • 谢谢亚历克斯。效率部分是我的强迫行为,部分是过早的优化。我完全打算预先生成缓冲区作为一次性后台初始化过程。但我想有效地做到这一点,因为稍后我正在考虑能够从 440Hz 的 A4 参考音高更改为 432Hz,并且不希望在生成缓冲区列表时出现明显的停顿。
    • 那么你只需要做一次。生成 1hz 正弦波,主要使用方法 B。
    猜你喜欢
    • 2012-07-11
    • 2018-10-26
    • 1970-01-01
    • 2012-01-08
    • 2016-02-21
    • 2015-02-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多