HarmonyOS项目实战:调频声波App(三)播放声音 原创
作者:大李子
团队:坚果派
十年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的核心代码就讲解完成了。完整工程请参考这个链接