目录
该项目使用无源蜂鸣器实现静态音乐播放器,即需要用户手动添加歌曲节点,读者可自行扩展成动态节点,实现动态音乐播放器列表,主要实现功能如下:
支持歌曲的暂停与播放
支持上一首、下一首歌曲切换
支持自定义音乐播放模式:顺序播放、随机播放、单曲循环
支持自定义设定音乐音量大小(0-100)
主要运用知识点:
定时器配置与使用
PWM配置与使用
C语言结构体、结构体嵌套、枚举使用
C语言指针、一维数组、二维数组、指针数组使用
C语言链表使用
源码中支持的库函数清单如下:
void beep_init(void); /* 无源蜂鸣器初始化 */
void beep_handler(void); /* 蜂鸣器音乐播放处理函数 定时执行 */
void music_init(void); /* 播放器初始化 */
void music_pause_playback(void); /* 暂停播放 */
void music_resume_playback(void); /* 恢复播放 */
void music_set_volume(unsigned char volume); /* 设置音量大小 */
unsigned char music_get_volume(void); /* 获取音量大小 */
void music_set_mode(ENUM_MUSIC_MODE_TYPEDEF mode); /* 设置播放模式 */
ENUM_MUSIC_MODE_TYPEDEF music_get_mode(void); /* 获取播放模式 */
void music_switch_previous(void); /* 播放上一首 */
void music_switch_next(void); /* 播放下一首 */
蜂鸣器(音乐播放器)驱动源码下载:无源蜂鸣器实现音乐播放器功能
一、音乐简谱相关知识
简谱是一种用简单符号表示音乐音高和节奏的音乐记谱方法。关于简谱中的相关知识点统计如下:
** 1. 音符**:简谱中用不同形状的符号表示不同音高的音符。常见的音符有:C、D、E、F、G、A、B。它们分别代表了音阶中的不同音名。
** 2. 节拍**:节拍是音乐中的基本时间单位,用来划分音乐的节奏。在简谱中,节拍可以用不同的符号和线条表示,如四分音符、八分音符等。
** 3. 拍号**:简谱中的拍号用来表示每小节中的拍数,常见的拍号有2/4、3/4、4/4等。这些拍号告诉演奏者每小节有多少拍和每拍的时值。
** 4. 调号**:调号在简谱中用来表示音乐作品所采用的调性。调号可以影响乐谱中所有音符的音高,使其适应特定的音阶。
** 5. 连线**:在简谱中,如果需要表示音符的音长超过一个小节,可以使用连线将两个相同音符连接起来,延长音符的时值。
** 6. 休止符**:除了音符之外,简谱还包括用来表示休止的符号。休止符用来表示音乐中的停顿或静默。
1、音符
在简谱中,用以表示音的高低及相互关系的基本符号,为七个阿拉伯数字:即1、2、3、4、5、6、7,唱作do、re、mi、fa、sol、la、si,称为唱名。
图1 音符说明
注意:一个音符由这个音的音高和对应的时值组成。
在简谱中,如果音符时值的长短用短横线“-”表示,就称为单纯音符。单纯音符除四分音符外,有以下两种形式:
**1. **在基本音符右侧加记一条短横线
表示增长原音符时值的一倍。这类加记在音符右侧、使音符时值增长的短横线,称为增时线。**增时线越多,音符的时值越长。**
**2. **在基本音符下方加记一条短横线
** 表示缩短原音符时值的一半。这类加记在音符下方、使音符时值缩短的短横线,称为减时线。减时线越多,音符的时值越短。**
图2 单纯音符说明
在简谱中,加记在单纯音符的右侧的,使音符时值增长的小圆点"·",称为附点。加记附点的音符称为附点音符,附点本身并无一定的长短,其长短由前面的单纯音符来决定。**附点音符会增长原音符时值的一半**,常用于四分音符和小于四分音符的各种音符之后。
图3 附点音符说明
2、音调
在一些其他乐谱中,可能会存在一些更高或更低的音,如:
1.在基本音符上方加记一个"·",表示该音升高一个八度,称为高音;
2.加记两个":",则表示该音升高两个八度,称为倍高音。
3.在基本音符下方加记一个"·",表示该音降低一个八度,称为低音;
4.加记两个":",则表示该音降低两个八度,称为倍低音。
图4 音调说明
3、识读简谱
接下来我将以儿歌《两只老虎》的简谱来为大家简单讲解。
图5 《两只老虎》简谱
由上图的简谱可知,该乐谱演奏使用的是C调,且每个音符使用节拍为一拍(4/4)。例如,第一个音符(1)代表使用C调音阶中的第一个音(do),且四分音符的时值为4。因此如果我们想用无源蜂鸣器演奏出上图中的这样的一首儿歌,我们需要:
1. 找到简谱中C调的音符对应的蜂鸣器频率(确定音调对应的频率)
图6 C调对应的蜂鸣器频率
2. 确定蜂鸣器演奏一拍所需的时间(即确定一个音调对应的节拍数)
源代码中我给定1/4拍为200ms,那么一拍需要的时间为800ms(用户也可根据歌曲的节奏自行设计)。
3.创建结构体确定一个音符所需的两个属性(音调频率、节拍数)
/* 音符 */
typedef struct {
unsigned short frequency; /* 音调 -- 表现形式为蜂鸣器频率 */
unsigned char duration; /* 节拍 -- 表现形式为重复次数 */
} STRUCT_MUSIC_NOTE_TYPEDEF;
4.将《两只老虎》简谱的每个音符使用结构体数组编写好代码
下面代码中的{523, 4}代表do的频率是523Hz,由于定义的基准节拍为1/4拍,歌曲中一个音符是4/4拍,因此这里填写4。
/* 两只老虎 歌单 C调 4/4 */
STRUCT_MUSIC_NOTE_TYPEDEF twoTigers_notes[32] = {
/* 两只老虎 */
{523, 4}, {587, 4}, {659, 4}, {523, 4},
/* 两只老虎 */
{523, 4}, {587, 4}, {659, 4}, {523, 4},
/* 跑得快 */
{659, 4}, {698, 4}, {784, 8},
/* 跑得快 */
{659, 4}, {698, 4}, {784, 8},
/* 一只没有眼睛 */
{784, 3}, {880, 1}, {784, 3}, {698, 1}, {659, 4}, {523, 4},
/* 一只没有耳朵 */
{784, 3}, {880, 1}, {784, 3}, {698, 1}, {659, 4}, {523, 4},
/* 真奇怪 */
{523, 4}, {784, 4}, {523, 8},
/* 真奇怪 */
{523, 4}, {784, 4}, {523, 8},
};
5.创建静态歌单列表
这里我以另外两首歌《生日快乐》和《私奔》(看完房客的后遗症,哈哈哈)为例,创建结构体数组如下:
/* 生日快乐 歌单 F调 3/4 -->目前使用的是C调 */
STRUCT_MUSIC_NOTE_TYPEDEF birthday_notes[25] = {
/* 祝你生日快乐 */
{392, 3}, {392, 3}, {440, 6}, {392, 6}, {523, 6}, {494, 12},
/* 祝你生日快乐 */
{392, 3}, {392, 3}, {440, 6}, {392, 6}, {587, 6}, {523, 12},
/* 祝你生日快乐 */
{392, 3}, {392, 3}, {784, 6}, {659, 6}, {523, 6}, {494, 6}, {466, 6},
/* 祝你生日快乐 */
{698, 4}, {698, 1}, {659, 3}, {523, 3}, {587, 6}, {523, 12},
};
/* 私奔 歌单 A调 4/4 -->目前使用的是C调 */
STRUCT_MUSIC_NOTE_TYPEDEF elope_notes[41] = {
/* 把青春献给 */
{392, 1}, {784, 2}, {784, 2}, {784, 2}, {784, 2}, {784, 4}, {659, 2}, {587, 2},
/* 身后那座 */
{587, 2}, {659, 2}, {659, 2}, {784, 2}, {784, 2}, {784, 2}, {880, 4},
/* 辉煌的都市 */
{523, 4}, {523, 4}, {440, 2}, {523, 4}, {440, 2},
/* 为了这个 */
{440, 2}, {523, 2}, {523, 2}, {523, 2}, {523, 2}, {440, 2}, {392, 2},
/* 美梦我们 */
{392, 2}, {523, 2}, {523, 2}, {523, 2}, {523, 2}, {784, 6},
/* 付出着代价 */
{659, 2}, {587, 2}, {523, 2}, {523, 2}, {523, 2}, {659, 2}, {523, 2}, {587, 2},
};
然后将三首歌的结构体数组存放到结构体指针数组中,方便我们后续使用链表进行索引
STRUCT_MUSIC_NOTE_TYPEDEF *notes[SONG_MAX] = {
birthday_notes, /* 生日快乐 */
twoTigers_notes,/* 两只老虎 */
elope_notes /* 私奔 */
};
这里面还引用了枚举值SONG_MAX,该枚举用于定义我们的歌曲列表中有哪些歌曲,枚举代码说明如下:
/* 歌曲曲目定义 */
typedef enum {
SONG_BIRTHDAY, /* 歌曲 生日快乐 */
SONG_TWO_TIGER, /* 歌曲 两只老虎 */
SONG_ELOPE, /* 歌曲 私奔 */
SONG_MAX,
} ENUM_MUSIC_SONG_TYPEDEF;
为什么需要创建这样的一个枚举类型呢?是为了方便我们后续找到我们的歌曲列表中有多少首歌,当我们新增一首歌之后,只需要将歌曲的枚举声明添加到SONG_ELOPE之后,SONG_MAX之前,这样当我们每次调用SONG_MAX就能获取到歌曲里有多少首歌啦!
二、音乐播放器实现过程
1、 无源蜂鸣器初始化配置
无源蜂鸣器配置主要分为两个部分:GPIO引脚配置与复用的定时器通道配置,代码如下:
/* 无源蜂鸣器使用引脚 */
#define BEEP_RCC_CLK RCC_APB2Periph_GPIOB
#define BEEP_GPIO_PORT GPIOB
#define BEEP_GPIO_PIN GPIO_Pin_1
/**
* @功能描述 GPIO基础功能初始化
* @入口参数 无
* @输出参数 无
*/
static void beep_gpio_config(void)
{
RCC_APB2PeriphClockCmd(BEEP_RCC_CLK | RCC_APB2Periph_AFIO, ENABLE); /* 使能端口时钟并开启复用时钟 */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = BEEP_GPIO_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(BEEP_GPIO_PORT, &GPIO_InitStructure);
}
/**
* @功能描述 定时器复用功能PWM初始化
* @入口参数 无
* @输出参数 无
*/
static void beep_pwm_config(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); /* 开启时钟 */
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; /* 定时器初始化 */
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; /* 分频因子 */
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; /* 向上计数模式 */
TIM_TimeBaseInitStruct.TIM_Period = 7200 - 1; /* 自动重装载值 */
TIM_TimeBaseInitStruct.TIM_Prescaler = 5 - 1; /* 预分频值 */
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct); /* 完成定时器初始化 */
TIM_OCInitTypeDef TIM_OCInitStruct; /* 定时器通道初始化 */
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; /* 初始化输出比较 */
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStruct.TIM_Pulse = 0;
TIM_OC4Init(TIM3, &TIM_OCInitStruct); /* 定时器通道x初始化 */
TIM_OC4PreloadConfig(TIM3, TIM_OCPreload_Enable); /* OCx预装载寄存器使能 */
TIM_ARRPreloadConfig(TIM3, ENABLE);
TIM_Cmd(TIM3, ENABLE);
}
/**
* @功能描述 无源蜂鸣器初始化
* @入口参数 无
* @输出参数 无
*/
void beep_init(void)
{
beep_gpio_config();
beep_pwm_config();
}
2、 蜂鸣器频率、占空比、使能配置
频率配置主要用于设置蜂鸣器的输出音调,即可以演奏出歌曲中的任意音调;占空比配置主要用于设置蜂鸣器的输出音量,即通过占空比控制蜂鸣器喇叭输出的高低;使能配置主要用于设置蜂鸣器是否输出,即蜂鸣器是否需要播放当前的音乐(切换播放与暂停功能的由来)。该部分代码如下:
/**
* @功能描述 设置蜂鸣器输出音量
* @入口参数 sound - 音量 0-100
* @输出参数 无
*/
static void beep_set_sound(unsigned char sound)
{
TIM_SetCompare4(TIM3, sound*7200/100);
}
/**
* @功能描述 设置蜂鸣器输出频率
* @入口参数 freq 频率
* @输出参数 无
*/
static void beep_set_freq(unsigned short freq)
{
TIM_PrescalerConfig(TIM3, 72000000/7200/freq, TIM_PSCReloadMode_Immediate);
}
/**
* @功能描述 设置蜂鸣器是否输出
* @入口参数 enable true-使能 false-失能
* @输出参数 无
*/
static void beep_enable(bool enable)
{
if(enable == true) {
TIM_Cmd(TIM3, ENABLE);
}
else {
TIM_Cmd(TIM3, DISABLE);
}
}
/**
* @功能描述 设置蜂鸣器播放音乐的音量 音调 使能
* @入口参数 sound - 音量 0-100
* @入口参数 freq - 音调(蜂鸣器频率)
* @入口参数 enable - 开/关蜂鸣器
* @输出参数 无
*/
void beep_settings(unsigned char sound, unsigned short freq, bool enable)
{
beep_set_sound(sound);
beep_set_freq(freq);
beep_enable(enable);
}
3、 音乐播放器列表清单初始化
接着我们就需要定义我们的静态歌曲列表中有哪些歌曲了,代码如下:
STRUCT_BEEP_MUSIC_TYPEDEF myMusic[SONG_MAX] = {
{true, 10, 0, 0, 0, 0, 25, "Happy Birthday", 0, 0, 0},
{true, 50, 0, 0, 0, 0, 32, "Two Tigers", 0, 0, 0},
{true, 80, 0, 0, 0, 0, 41, "Elope", 0, 0, 0},
};
这里使用了一个新的结构体STRUCT_BEEP_MUSIC_TYPEDEF,其具体定义如下:
/* 音谱 */
typedef struct BEEP_MUSIC{
bool isPause; /* 暂停/恢复音乐播放标志位 true-暂停 false-恢复 */
unsigned char volume; /* 歌曲播放的音量大小 0-100 */
unsigned char repeats; /* 当前节拍 需要重复的次数 */
unsigned char repeat_cnt; /* 当前节拍 重复次数计数值 */
unsigned short curr_tone; /* 当前的音调 */
unsigned short tone_cnt; /* 当前音调所处的计数值 */
unsigned short total_tone; /* 总音调的个数 */
const char* song_title; /* 歌曲名称 */
STRUCT_MUSIC_NOTE_TYPEDEF *notes; /* 歌曲音符表 */
struct BEEP_MUSIC *pre_music; /* 指向上一首歌曲 */
struct BEEP_MUSIC *next_music; /* 指向下一首歌曲 */
} STRUCT_BEEP_MUSIC_TYPEDEF;
这个结构体用于描述音乐播放相关的信息,包括音量、播放状态、音调等,同时也包含了链表结构,因此也就可以串联多首歌曲了。
4、 音乐播放器初始化
static STRUCT_BEEP_MUSIC_TYPEDEF *cur_music; /* 当前播放歌曲指针 */
static ENUM_MUSIC_MODE_TYPEDEF music_mode; /* 歌单播放模式 */
/**
* @功能描述 音乐播放器初始化函数
* @入口参数 无
* @输出参数 无
*/
void music_init(void)
{
unsigned char i;
/* 加载所有歌曲的歌单数据 */
for(i=0; i<SONG_MAX; i++) {
/* 为notes成员分配内存 */
myMusic[i].notes = (STRUCT_MUSIC_NOTE_TYPEDEF *)malloc(myMusic[i].total_tone * sizeof(STRUCT_MUSIC_NOTE_TYPEDEF));
/* 使用memcpy函数复制歌曲音符数据 */
memcpy(myMusic[i].notes, notes[i], myMusic[i].total_tone * sizeof(STRUCT_MUSIC_NOTE_TYPEDEF));
}
/* 使用循环链表连接所有的歌曲 */
for(i=0; i<SONG_MAX; i++) {
(i == 0) ? (myMusic[0].pre_music = &myMusic[SONG_MAX-1]) : (myMusic[i].pre_music = &myMusic[i-1]);
(i == SONG_MAX-1) ? (myMusic[SONG_MAX-1].next_music = &myMusic[0]) : (myMusic[i].next_music = &myMusic[i+1]);
}
cur_music = &myMusic[0]; /* 初始化歌曲指针指向第一首歌 */
music_mode = MODE_RANDOM; /* 初始化播放模式为顺序播放模式 */
}
5、 设计音乐播放器处理函数
/**
* @功能描述 蜂鸣器音乐播放器入口处理函数
* @入口参数 无
* @输出参数 无
*/
void beep_music_handler(void)
{
/* 判断歌曲不存在 或 按下暂停键 就退出播放 */
if(cur_music == NULL || cur_music->isPause == true) {
beep_enable(false);
return;
}
if(cur_music->curr_tone >= cur_music->total_tone) {
music_switch_next();
}
if(cur_music->repeat_cnt >= cur_music->repeats) {
cur_music->repeat_cnt = 0;
cur_music->curr_tone ++;
cur_music->repeats = cur_music->notes[cur_music->curr_tone].duration;
}
beep_settings(cur_music->volume, cur_music->notes[cur_music->curr_tone].frequency, 1);
cur_music->repeat_cnt ++;
}
这段代码是一个处理蜂鸣器音乐播放的函数
beep_music_handler
,主要功能如下:
首先判断当前的歌曲是否存在(即
cur_music
是否为NULL)或者当前歌曲是否处于暂停状态(isPause
为true),如果是,则停止蜂鸣器的播放并退出函数。如果当前音符的索引
curr_tone
大于等于总音符数total_tone
,则切换到下一首歌曲(调用music_switch_next
函数)。如果当前歌曲的重复次数
repeat_cnt
大于等于设定的重复次数repeats
,则重置重复计数器,并将当前音符索引指向下一个音符,同时更新重复次数为下一个音符的持续时间。根据当前音符的音量和频率设置蜂鸣器的音调和音量,播放该音符。
增加当前歌曲的重复计数器值。
这个函数实现了音乐播放器的核心逻辑,包括切换音符、切换歌曲、设置音调和音量等操作,以实现音乐的连续播放。
6、 其他业务代码(暂停、播放、上一曲、下一曲、音量调节)
针对这部分的业务代码就比较简单了,只要能够理解STRUCT_BEEP_MUSIC_TYPEDEF结构体就知道这部分内容是在干嘛了,代码如下:
/**
* @功能描述 音乐播放器 暂停播放音乐
* @入口参数 无
* @输出参数 无
*/
void music_pause_playback(void)
{
cur_music->isPause = true;
}
/**
* @功能描述 音乐播放器 恢复播放音乐
* @入口参数 无
* @输出参数 无
*/
void music_resume_playback(void)
{
cur_music->isPause = false;
}
/**
* @功能描述 音乐播放器 设置音量大小
* @入口参数 volume - 音量 0-100
* @输出参数 无
*/
void music_set_volume(unsigned char volume)
{
cur_music->volume = volume;
}
/**
* @功能描述 音乐播放器 获取音量大小
* @入口参数 无
* @输出参数 volume - 音量 0-100
*/
unsigned char music_get_volume(void)
{
return (cur_music->volume);
}
/**
* @功能描述 音乐播放器 设置歌曲播放模式
* @入口参数 mode - 模式 顺序 单曲 随机
* @输出参数 无
*/
void music_set_mode(ENUM_MUSIC_MODE_TYPEDEF mode)
{
music_mode = mode;
}
/**
* @功能描述 音乐播放器 获取歌曲播放模式
* @入口参数 无
* @输出参数 mode - 模式 顺序 单曲 随机
*/
ENUM_MUSIC_MODE_TYPEDEF music_get_mode(void)
{
return (music_mode);
}
/**
* @功能描述 音乐播放器 切换上一首
* @入口参数 无
* @输出参数 无
*/
void music_switch_previous(void)
{
if(cur_music->pre_music != NULL) {
/* 先清除正在播放的歌曲进度 */
cur_music->isPause = true;
cur_music->repeats = 0;
cur_music->repeat_cnt = 0;
cur_music->curr_tone = 0;
cur_music->tone_cnt = 0;
cur_music = cur_music->pre_music;
cur_music->isPause = false;
}
}
/**
* @功能描述 生成指定范围内的随机整数
* @入口参数 min-max 指定范围
* @输出参数 无
*/
static int myRandom(int min, int max)
{
return min + rand() % (max - min + 1);
}
/**
* @功能描述 音乐播放器 切换下一首
* @入口参数 无
* @输出参数 无
*/
void music_switch_next(void)
{
if(cur_music->next_music != NULL) {
/* 清除正在播放的歌曲进度 */
cur_music->repeats = 0;
cur_music->repeat_cnt = 0;
cur_music->curr_tone = 0;
cur_music->tone_cnt = 0;
switch(music_mode) {
case MODE_ORDER: {
cur_music->isPause = true;
cur_music = cur_music->next_music;
cur_music->isPause = false;
}break;
case MODE_SONGLE: {
}break;
case MODE_RANDOM: {
cur_music->isPause = true;
cur_music = &myMusic[myRandom(0, SONG_MAX-1)];
cur_music->isPause = false;
}break;
default: break;
}
}
}
三、使用方法
1、初始化无源蜂鸣器
找到一个无源蜂鸣器,并选择使用STM32的引脚(注意需要带定时器复用功能的,如本例中的PB1),然后在main函数中调用函数beep_init()进行蜂鸣器的初始化。
2、创建歌曲简谱
找希望播放的歌曲,创建对应的结构体数组,创建步骤见 **1.3节 识读乐谱。**
** *这里注意需要有四个地方需要修改:①beep.h中的歌曲枚举需要新增自己的歌曲枚举定义;②创建歌曲对应的结构体数组;③notes指针数组中需要添加对应的结构体数组;④myMusic结构体数组中需要添加对应的歌曲初始化内容。
3、初始化音乐播放器
然后需要在main函数中调用music_init()函数去初始化我们的静态音乐播放列表。
注意:music_init()函数中初始化歌曲默认指向的第1首歌,并且播放模式为顺序播放模式。
4、 创建定时器运行音乐播放器处理函数
这里我们需要创建一个任务定时器用于定时运行函数beep_music_handler(),定时时间为我们之前给定的200ms。当然如果不想创建定时器的话呢,也可以使用延时函数进行处理。
注意:使用延时函数处理的话可能会存在任务的运行会受到延时函数的阻塞,影响系统的运行流畅度,因此推荐使用定时器。
5、 调用源码中的接口函数实现音乐播放器的功能
我们可以创建按键扫描与键值获取函数,用于使用按键控制音乐的播放与暂停、上一曲与下一曲、播放模式的切换等功能。
6、 测试代码
int main(void)
{
delay_init();
usart1_init(115200);
basic_tim_init();
/* 外设初始化 */
led_init();
key_init();
lcd_init();
power_init();
beep_init();
music_init();
lcd_clearGram();
lcd_refreshGram();
timer_createTask(T_TASK1, key_scan_10ms, 10); /* 按键扫描任务 10ms */
timer_createTask(T_TASK2, beep_music_handler, 200); /* 音乐播放器处理任务 200ms */
while(1) {
switch(key_getValue()) {
case KEY_OK_S: {
music_pause_playback();
}break;
case KEY_DOWN_S: {
music_resume_playback();
}break;
case KEY_LEFT_S: {
music_switch_previous();
}break;
case KEY_RIGHT_S: {
music_switch_next();
}break;
}
}
}
END
版权归原作者 JJ & NN 所有, 如有侵权,请联系我们删除。