1、项目介绍
本项目是基于RP2350B核心板和电子琴扩展板实现了一个模拟电子琴,音域从C3到C6横跨三个8度音域,每个8度音域中包含了5个半度音,模拟了钢琴的琴键分布。可以使用电子琴扩展板右侧的按键来切换音域,使用电子琴扩展板中间拨动开关来切换选择使用蜂鸣器或者喇叭播放,使用核心板上的用户按键来开始、停止录音和重播录音。
2、硬件介绍
2.1 RP2350B核心板
RP2350的性能强大,包含双核Arm处理器 + 双核RISC-V处理器,以及可编程IO(PIO),该芯片不仅支持通用的外设总线(I2C、SPI以及UART)访问,还可以通过适当的配置让PIO访问高速外设,在很多场景下能够完成FPGA才能实现的功能。核心板板载了很多外设,包括:8颗单色LED、2个三色LED、2个7段数码管、4个拨码开关、4个轻触按键、一颗单色LED和一个姿态传感器,单是板载的外设就可以做很多小项目了。RP2350B核心板采用Type C的USB接口,支持USB2.0高速数据传输(最高12Msps),板上有一颗开关稳压芯片XT3406将USB接口的5V(Vbus上得到,会在4.75V-5V之间)转化为RP2350B以及其它数字器件需要的3.3V直流电压,板上有一颗LED为3.3V电压指示灯。
2.2 电子琴扩展板
电子琴扩展板巧妙地利用了两层电路板结构,实现了电子琴的触感。有别于其它电子琴只用单调发音的蜂鸣器,这个电子琴在保留了蜂鸣器的基础上(用于对比),直接采用了iPhone上使用的扬声器,它的音质洪亮、圆润,只要能给它足够好的信号,它都能完美地播放出来,别说可以播放电子琴、钢琴的声音,即便小提琴、摇滚乐的声音它也能绘声绘色地播放出来,当然前提是要给它提供相应的模拟信号。
3、方案框图和项目设计思路介绍
3.1 方案框图
本项目的方案框图如下图所示,以RP2350B为核心主控,循环检测琴键、用户按键和播放器选择开关,分别对应琴音播放、开始停止和播放录音、选择使用蜂鸣器或者喇叭播放功能,数码管显示当前是否在录音。
3.2 设计思路
3.2.1 音频播放模块
琴音播放可以选择使用蜂鸣器或者喇叭播放。蜂鸣器播放时直接使用对应频率PWM驱动即可;而使用喇叭播放时首先需要使用PWM加低通滤波器实现模拟DAC(具体相关原理可以参考我的专栏),然后使用MATLAB按一定参数生成所有频率对应音频数据,最后再使用模拟DAC加隔直电容和功放来播放对应的音频数据即可。
3.2.2 按键扫描模块
包含琴键、音域扩展按键、播放器选择开关和录制播放等按键的扫描,分别完成琴音播放、音域切换、选择使用蜂鸣器或者喇叭播放、录音的启停和播放的功能。
3.2.3 数码管与LED模块
数码管用于展示当前是否在录制音频,正在录制音频是数码管会显示“RC”;LED用来展示当前使用的音域,高8度音域时最上方LED亮起,中8度音域时中间LED亮起,低8度音域时最下方LED亮起。
4、软件流程图和关键代码介绍
4.1 软件流程图
软件流程图如下图所示,首先是系统初始化,主要是GPIO和PWM的初始化。之后进入主循环,进行按键扫描,根据不同按键执行不同的功能。
4.2 关键代码介绍
4.2.1 按键扫描
按键扫描的主要代码实现如下所示,每隔一毫秒扫描一次,包含琴键、音域扩展按键、播放器选择开关和录制播放等按键的扫描,分别完成琴音播放、音域切换、切换播放器、录音的启停和播放的功能。
//1ms执行一次
void key_scan(void)
{
static uint8_t key_pressed[32] = {0};
static uint8_t key_lower_pressed = 0;
static uint8_t key_upper_pressed = 0;
static uint8_t key_player_beep = 0;
static uint8_t key_player_speaker = 0;
for(uint8_t gpio=4;gpio<17;gpio++){
if(gpio_get(gpio)==0)key_pressed[gpio]++;
else key_pressed[gpio]=0;
if(key_pressed[gpio]>10){
//按一次按键只执行一次
if(key_pressed[gpio]==11)tone_list_add(gpio);
key_pressed[gpio] = 99;
}
}
//切换播放器
if(gpio_get(29) == 0 ){
key_player_beep++;
key_player_speaker=0;
}
else{
key_player_speaker++;
key_player_beep=0;
}
if(key_player_beep > 10){
//只执行一次
player_dedv_type = PLAYER_BEEP;
if(key_player_beep==11)piano_play_speaker_stop();
key_player_beep = 99;
}
if(key_player_speaker > 10){
//只执行一次
player_dedv_type = PLAYER_SPEAKER;
if(key_player_speaker==11){
piano_play_beep_stop();
piano_play_speaker_init();
}
key_player_speaker = 99;
}
//切换音程
if(gpio_get(17) == 0)key_lower_pressed++;
else key_lower_pressed = 0;
if(gpio_get(18) == 0)key_upper_pressed++;
else key_upper_pressed = 0;
if(key_lower_pressed > 10){
//按一次按键只执行一次
if(key_lower_cb!=NULL && key_lower_pressed==11)key_lower_cb();
key_lower_pressed = 99;
}
if(key_upper_pressed > 10){
//按一次按键只执行一次
if(key_upper_cb!=NULL && key_upper_pressed==11)key_upper_cb();
key_upper_pressed = 99;
}
}
void adc_key_scan(void)
{
static uint8_t adc_stable_least = 0;
static uint8_t adc_list[8] = {0};
static uint8_t index = 0;
uint16_t adc_sum = 0;
uint16_t adc_mean = 0;
uint16_t adc_var = 0;
int16_t adc_sub = 0;
adc_select_input(7);
uint16_t adc = adc_read()>>4;
if(index<8){
adc_list[index] = adc;
index++;
}
else{
for(uint8_t i=0; i<7; i++)
{
adc_list[i] = adc_list[i+1];
adc_sum += adc_list[i];
}
adc_list[7] = adc;
adc_mean = (adc_sum+adc_list[7])>>3;
for(uint8_t i=0; i<8; i++)
{
adc_var += (adc_mean - adc_list[i])*(adc_mean - adc_list[i]);
}
// printf("adc:%d,mean:%d,var:%d,stable_list:%d\r\n",adc,adc_mean,adc_var,adc_stable_least);
if(adc_var < 16){
adc_sub = adc_stable_least - adc;
//14 28 56 112
if(adc_sub > 100){//开始或停止录制
printf("adc key 1 pressed");
if(is_paino_recording()==0)piano_start_record();
else piano_stop_record();
}
else if(adc_sub > 50){//播放记录的音频
printf("adc key 2 pressed");
piano_start_replay();
}
else if(adc_sub > 20){
printf("adc key 3 pressed");
}
else if(adc_sub > 10){
printf("adc key 4 pressed");
}
adc_stable_least = adc;
}
}
}
4.2.2 音频播放
琴音播放主要实现代码如下所示,在扫描到相应琴键按下时将对应音频添加到待播放列表,再根据采样率按一定时间间隔输出对应音频数据。和弦功能也在这里实现,当两个琴键按下的时间间隔很短时认为此时需要和弦,和弦方法是将两个音频数据对齐后进行平均输出,
uint16_t get_audio_data()
{
uint16_t data = 2048;
if(play_index<TONE_SAMPLE_COUNT){
if(flag_mix==0){
data = piano_data[play_tone[0]][play_index];
}
else{
data = (piano_data[play_tone[0]][play_index] + piano_data[play_tone[1]][play_index])/2;
}
play_index++;
}
return data;
}
void tone_list_add(uint8_t gpio)
{
uint8_t tone = audio_range*12 + gpio-4;
printf("tone_list_add %d",tone);
if(paino_record.flag_recording == 1){
uint8_t index = paino_record.record_index;
if(index==0){
paino_record.time_start = get_milliseconds();
paino_record.gpio[index] = gpio;
paino_record.time[index] = 0;
paino_record.record_index++;
}
else{
paino_record.gpio[index] = gpio;
paino_record.time[index] = get_milliseconds()-paino_record.time_start;
paino_record.record_index++;
}
}
if(get_player_device_type()==PLAYER_SPEAKER){
uint32_t time_now = get_milliseconds();
if(time_now - least_time_tone_add < 80){//两个按键按下时间间隔很短,视为同时按下
if(flag_mix==0){//按多个只将前两个和弦
flag_mix = 1;
play_index = 0;
play_tone[1] = tone;
}
}
else{
flag_mix = 0;
play_index = 0;
play_tone[0] = tone;
}
least_time_tone_add = time_now;
}
else{//使用蜂鸣器播放
piano_play_beep(piano_freq_list[tone]);
time_beep_play = 1;
}
}
bool timer_callback(repeating_timer_t *rt)
{
if(get_player_device_type()==PLAYER_SPEAKER){
time_beep_play = 0;
pwm_set_gpio_level(21, get_audio_data());
}
else {//使用蜂鸣器播放
if(time_beep_play>0)time_beep_play += time_inc;
if(time_beep_play>PIANO_BEEP_PLAY_TIME){
pwm_set_enabled(slice_num_beep, false);
time_beep_play = 0;
}
// pwm_set_gpio_level(21, 2048);//2048是直流分量的值
}
return true; // keep repeating
}
void piano_play_beep(float freq)
{
gpio_set_function(PIANO_BEEP_PIN, GPIO_FUNC_PWM);
slice_num_beep = pwm_gpio_to_slice_num(PIANO_BEEP_PIN);
pwm_config config = pwm_get_default_config();
pwm_config_set_clkdiv(&config, 75.f);//分频到2M,使之能够尽量覆盖所有频率
uint16_t count_top = 2000000.f/freq;
// printf("pwm count top=%d",count_top);
pwm_config_set_wrap(&config, count_top);
pwm_init(slice_num_beep, &config, true);
pwm_set_gpio_level(PIANO_BEEP_PIN, count_top/2);
}
void piano_play_speaker_init(void)
{
static uint8_t flag_inited = 0;
if(flag_inited==1){
cancel_repeating_timer(&timer);
}
gpio_set_function(PIANO_PWM_PIN, GPIO_FUNC_PWM);
slice_num_speaker = pwm_gpio_to_slice_num(PIANO_PWM_PIN);
pwm_config config = pwm_get_default_config();
pwm_config_set_wrap(&config, 4095);
pwm_init(slice_num_speaker, &config, true);
if (!add_repeating_timer_us(-(1000000/SAMPLE_RATE), timer_callback, NULL, &timer)) {
printf("Failed to add timer");
return;
}
flag_inited = 1;
}
4.2.3 模拟钢琴音
使用MATLAB获取模拟钢琴音数据的代码如下所示,使用到了基频以及前10个谐波,并且还模拟了钢琴特有的衰减特性的包络面,使模拟的琴音更加接近真实的琴音。
% gen_tone_data - 根据参数生成声音数据
% tone_freq: 音调频率
% duration: 持续时间(秒)
% fs: 采样率
% bit_ddepth: 采样深度
function tone_data = gen_tone_data(tone_freq, duration, fs, bit_depth)
t = 0:1/fs:duration-1/fs; % 时间向量
y = zeros(size(t));%先生成0矩阵
% 基频和前10个谐波
numHarmonics = 10;
for h = 1:numHarmonics
freq = tone_freq * h;
% 振幅随谐波次数衰减,模拟钢琴音色特性
amplitude = 1 / h^1.2;
% 相位随机化,增加自然感
phase = rand * 2 * pi;
% 每个谐波添加不同的衰减时间
decay = 0.8 / h^0.5;
y = y + amplitude * sin(2 * pi * freq * t + phase) .* exp(-t/decay);
end
% 添加钢琴特有的击键衰减特性
attack = 0.01;
decay = 0.3;
sustain = 0.8;
release = 0.5;
% 包络生成
env = zeros(size(t));
env(t <= attack) = t(t <= attack) / attack;
env(t > attack & t <= attack+decay) = 1 - (1-sustain) * (t(t > attack & t <= attack+decay) - attack) / decay;
env(t > attack+decay & t <= duration-release) = sustain;
env(t > duration-release) = sustain * (1 - (t(t > duration-release) - (duration-release)) / release);
% 应用包络
y = y .* env;
% 归一化
y = y / max(abs(y));
% 先添加直流分量将数据范围从(-1,1)->(0,2),再放大到DAC位数(注意放大倍数,y的数值范围为0~2)范围
% 注意原数据为double,需要转化为短整型
tone_data = int16(round((y+1) * power(2, bit_depth-1)));
end
5、实物功能展示
实机功能展示见顶部视频
6、总结
本次项目中遇到的难题主要是刚开始不清楚如何使用PWM驱动功放播放音频,后来学习到了PWM加低通滤波器实现模拟DAC的方法,有了模拟DAC之后就可以播放任意音频了,后边只需要使用MATLAB生成一下更真实的钢琴音数据即可。