HarmonyOS项目实战:调频声波App(三)播放声音 原创

大黑布林李子
发布于 2024-1-19 02:30
浏览
0收藏

作者:大李子
团队:坚果派
十年iOS,All in转鸿蒙

调频声波App(一)概述
调频声波App(二)UI
调频声波App(三)播放声音

生成声波

思路(可以跳过)

形成声波并播放是这个App的核心功能,如何实现这个功能,属实走了很多弯路。起初认为这是一个计算密集任务,在网上查到了一个生成正弦波并输出wav文件的C语言实现,并开了一个C工程来验证功能。可以成功调整声波频率,并生成wav文件。

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "sndfile.h"

#define SAMPLE_RATE 44100  // Sample rate in Hz
#define DURATION 5.0       // Duration in seconds
#define AMPLITUDE 0.5      // Amplitude of the sine wave
#define FREQUENCY 440.0    // Frequency in Hz

int main() {
    // Calculate the number of samples
    int num_samples = (int)(SAMPLE_RATE * DURATION);

    // Open the output file for writing
    SF_INFO sfinfo;
    sfinfo.samplerate = SAMPLE_RATE;
    sfinfo.channels = 1;  // Mono
    sfinfo.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
    SNDFILE* outfile = sf_open("sine_wave.wav", SFM_WRITE, &sfinfo);

    if (!outfile) {
        printf("Error: Unable to open output file\n");
        return 1;
    }

    // Generate and write the sine wave to the file
    double phase = 0.0;
    for (int i = 0; i < num_samples; i++) {
        double value = AMPLITUDE * sin(2.0 * M_PI * FREQUENCY * i / SAMPLE_RATE);
        if (sf_writef_double(outfile, &value, 1) != 1) {
            printf("Error writing to file\n");
            return 1;
        }
    }

    // Close the output file
    sf_close(outfile);

    printf("Sine wave generated and saved to 'sine_wave.wav'\n");

    return 0;
}

可以看到这段代码里面依赖三方库sndfile。所以起初为了把这段C代码放进App里,在native包上面研究了很久。包括怎么处理三方库sndfile的依赖,以及sndfile对其他库的依赖。尝试过直接集成源码,也尝试过编译不同处理器架构的so文件。但发现工作量太大,另外涉及到的技术栈不熟悉,花太多精力搞这个功能。之后换了个思路,找了一份不依赖三方库生成正弦波的C代码。

#include <stdio.h>
#include <stdint.h>
#include <math.h>

#define SAMPLE_RATE 44100   // Sample rate in Hz
#define DURATION 1          // Duration of the sine wave in seconds
#define AMPLITUDE 0.5       // Amplitude of the sine wave
#define FREQUENCY 440.0     // Frequency of the sine wave in Hz
#define NUM_CHANNELS 1      // Number of audio channels (1 for mono, 2 for stereo)

// Function to write a 16-bit PCM sample to a file
void write_sample(FILE *file, int16_t sample) {
    fwrite(&sample, sizeof(int16_t), 1, file);
}

int main() {
    FILE *wav_file;
    int16_t sample;
    double t, dt;

    // Open the WAV file for writing
    wav_file = fopen("sine_wave.wav", "wb");
    if (!wav_file) {
        fprintf(stderr, "Error opening WAV file for writing\n");
        return 1;
    }

    // Calculate the time step (inverse of sample rate)
    dt = 1.0 / SAMPLE_RATE;

    const uint32_t chunkSize = 16;
    const uint16_t audioFormat = 1;
    const uint16_t numChannels = NUM_CHANNELS;
    const uint32_t sampleRate = SAMPLE_RATE;
    const uint32_t byteRate = SAMPLE_RATE * NUM_CHANNELS * sizeof(int16_t);
    const uint16_t blockAlign = NUM_CHANNELS * sizeof(int16_t);
    const uint16_t bitsPerSample = 16;

    // Write WAV file header
    fprintf(wav_file, "RIFF----WAVEfmt ");    // Chunk ID and format
    fwrite(&chunkSize, 4, 1, wav_file);  // Chunk size (16 for PCM)
    fwrite(&audioFormat, 2, 1, wav_file);   // Audio format (1 for PCM)
    fwrite(&numChannels, 2, 1, wav_file);  // Number of channels
    fwrite(&sampleRate, 4, 1, wav_file);    // Sample rate
    fwrite(&byteRate, 4, 1, wav_file);  // Byte rate
    fwrite(&blockAlign, 2, 1, wav_file);  // Block align
    fwrite(&bitsPerSample, 2, 1, wav_file);   // Bits per sample

    fprintf(wav_file, "data----");  // Data sub-chunk

    // Generate and write sine wave samples
    for (t = 0; t < DURATION; t += dt) {
        sample = AMPLITUDE * (int16_t)(32767.0 * sin(2.0 * M_PI * FREQUENCY * t));
        write_sample(wav_file, sample);
    }

    // Close the WAV file
    fclose(wav_file);

    return 0;
}

这段代码可以直接放到native子工程里,并在js端调用。之后又花了很多精力研究了一下App文件沙盒的访问,使C语言生成的wav文件能被js访问到。然后通过AVPlayer播放wav文件。
然而,根据App的功能,需要在主界面拖动并连续调整声波频率。考虑到每次调整频率都要删除旧的wav,生成新的wav,效率可能不够。实际的验证下拉也发现频率调节会有延迟和杂音的问题。
于是,继续研究,深入阅读源码,发现整个代码的核心功能在for循环里。在// Write WAV file header注释段中,写入的是wav文件头,这段数据可以舍弃,舍弃以后的文件只有纯声波数据(pcm文件)。所以是否可以直接把声波数据播放出来呢?

最终方案(正文开始)

最终我在文档里找到了AudioRenderer,这个组件可以把声波数据直接播放出来。
创建一个AudioRendererPlayer类来控制音频的播放,以下是该类中的核心代码。本代码示例省略了很多细节,包括AudioRenderer的创建过程和写入声波数据的异步操作,为的是展示最核心的实现思路。完整源码请参考AudioRendererPlayer.ets

const renderModel: audio.AudioRenderer
const bufferSize = 800 // 1. bufferSize的大小经过了试验,取800是一个比较合适的数值。太大会导致一次写入的声波数据要放很久,在调整频率的时候会有延迟。太小的话,声音的播放会失败。
const data = new Int16Array(bufferSize)

for (let i = 0; i < bufferSize; i++) { // 2. 这是一段可以生成连续声波的循环,循环次数控制在bufferSize内,参数t连续重置
  data[i] = AMPLITUDE * (32767.0 * Math.sin(2.0 * Math.PI * this.frequency * this.t))
  this.t += dt;
  if (this.t >= 1.0 / this.frequency) { 
    this.t -= 1.0 / this.frequency;
  }
}

this.renderModel.write(data.buffer) // 3. 将生成出来的声波数据由AudioRenderer写入。

除了正弦波之外,我们还可以生成其他的波形,把data[i]的赋值提取一个方法,判断当前类中设置的波形类型,生成相应的声波数据。

data[i] = this.createWav()
private createWav(): number {
  switch (this.wavType) {
    case WaveType.SINE: {
      return AMPLITUDE * (32767.0 * Math.sin(2.0 * Math.PI * this.frequency * this.t))
    }
    case WaveType.SQUARE: {
      const wave = (this.t < 0.5 / this.frequency) ? AMPLITUDE * 32767 : -AMPLITUDE * 32767
      return wave * 0.3
    }
    case WaveType.TRIANGLE: {
      const dividend = this.t * this.frequency
      const divisor = 1.0
      const position = ((dividend % divisor) + divisor) % divisor

      // Determine the triangle wave value based on the position
      let wave: number
      if (position < 0.25) {
          wave = AMPLITUDE * 32767 * (4 * position);
      } else if (position < 0.75) {
          wave = AMPLITUDE * 32767 * (2 - 4 * position);
      } else {
          wave = AMPLITUDE * 32767 * (4 * position - 4);
      }
      return wave
    }
    case WaveType.SAWTOOTH: {
      const dividend = this.t * this.frequency
      const divisor = 1.0
      const position = ((dividend % divisor) + divisor) % divisor

      const wave = AMPLITUDE * 32767 * (2 * position - 1);
      return wave * 0.5
    }
  }
}

至此,本App的核心代码就讲解完成了。完整工程请参考这个链接

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2024-2-4 00:28:02修改
收藏
回复
举报
回复
    相关推荐