stm32 无源蜂鸣器实验 播放音乐 猪八戒背媳妇

前言

在8051及stm32各类教辅资料中,均有无源蜂鸣器相关的实验。可以通过单片机控制无源蜂鸣器发出指定频率和时长的声音,从而实现播放音乐的功能。
在以往的此类案例中,乐谱的谱写非常不方便,除了案例提供的乐谱数据外,学者要将一个其它的简谱转换成单片机可以播放的数据,基本不可能实现。另外在以往的案例中,音乐的播放也比较单调粗糙。
今天要分享的内容,是通过stm32f103x单片机控制无源蜂鸣器播放音乐《猪八戒背媳妇》片段,以此向大家展示如何可以自由的将简谱转换成单片机可以播放的程序数据。

效果展示

说教是比较枯燥的,我们先看下成品效果:

stm32f103 控制无源蜂鸣器播放音乐《猪八戒背媳妇》

Keil 工程

以上展示效果的完整的keil工程,见附件。

主程序展示

stm32程序中,我们以main.c中定义实现我们需要的功能,下面是上述视频效果的main.c程序

#include "stm32f10x.h"  // Device header
#include "tone.h"		//提供音乐播放接口
#include "music.h"		//提供简谱数据接口,可以方便的将简谱转换为程序数据
#include "delay.h"		//提供延时功能
#include "dyyOLED.h"	//提供oled显示功能

MusicalNotation_t song;

// 歌曲简谱
Note123_e song_notes[] = {	note1,		note6_l,	note3,		note5,
							note3,		note6_l,	note1,
							note6_l,	note1,		note6_l,	note1,		note3,
							note3,		note2,		note3,		note1,		note6_l,
							note3,		note5,		note6,		note6,
							note6,		note3,		note5,
							note3,		note5,		note3,		note5,		note6,	note6,
							note6,		note3,		note5,
							note5,		note6_l,	note5,		note6_l,
							note4,		note2,		note3,		note1,
							note2,		note2,
							note2,		note1,		note2,		note3,		note5,
							note6,		note3_h,
							note3,		note3_h,
							note3,		note3_h,	note3,		note3_h,
							note4,		note2,		note3,		note1,
							note2,		note2,
							note2,		note1,		note2,		note3,		note5,
							note6};

float song_beats[] = {		0.25,		1.0,		0.75,		0.25,
							0.5,		0.5,		1.0,
							0.25,		0.25,		0.25,		0.25,		1.0,
							0.25,		0.25,		0.25,		0.25,		1.0,
							0.75,		0.25,		0.5,		0.5,
							0.5,		0.5,		1.0,
							0.25,		0.25,		0.25,		0.25,		0.5,	0.5,
							0.5,		0.5,		1.0,
							0.5,		0.5,		0.5,		0.5,
							0.5,		0.25,		0.25,		1.0,
							1.0,		1.0,
							0.5,		0.25,		0.25,		0.5,		0.5,
							1.0,		1.0,
							1.0,		1.0,
							0.5,		0.5,		0.5,		0.5,
							0.5,		0.25,		0.25,		1.0,
							1.0,		1.0,
							0.5,		0.25,		0.25,		0.5,		0.5,
							2.0};


int main(void){
	delay_init();	//初始化延时函数
	toneInit(TIM2, TIM_Channel_1);	//配置定时器和输出端口,并以此端口驱动无源蜂鸣器发声
	oledInit();		//初始化 oled 功能

	char song_name[] = "zhuBaJie";
	float song_beatSpd_bpm = 88;	//以 4分音符 为准的拍速,如果乐曲是以 8分音符为基准定义的拍速,则需要 / 2 换算成以 4分音符 为准的拍速

	song.name = song_name;	//曲名
	song.beatSpd_bpm = song_beatSpd_bpm;	//以 4分音符为准的 拍速
	song.notationBase = note_c1;	//调式,note_c1 即为 C 调, note_d1 即为 D 调, 以此类推, note_g1 即为 G 调
	song.notes = song_notes;	//简谱的音符序列
	song.beats = song_beats;	//简谱每一个音符的拍数:如果单独一个音符,则为1, 如果有一个下划线,则为0.5;如果有两个下划线,则为 0.25; 如果后面有跟n个短横线,则 +n
	song.notesCount = sizeof(song_notes) / sizeof(song_notes[0]);	//计算音符数量
	song.beatsCount = sizeof(song_beats) / sizeof(song_beats[0]);	//计算节拍数量

	oledShowString(1, 1, "Music playing...");	//在 oled 屏上显示一行消息(第1行)
	while (1)
	{
		oledClearLane(2);	//清空 oled 屏第2行
		oledShowString(2,1,song.name);	//在 oled 屏第2行显示乐曲名称
		song.playIdx = 0;	//复位播放位置
		while (!playOver(&song)){	//如果没有播放完,则一直循环
			toneSing(getNoteFre(&song));	//获取简谱播放位置的音符对应的声音频率,并播放该频率的声音
			delay_ms(getNoteBeat(&song) * 60000.0f / song.beatSpd_bpm);		//获取简谱播放位置的音符时长,并延时时长的时间
			
			toneQuiet();	//停止音符的播放
			song.playIdx++;	//后移播放位置
			delay_ms(5);	//延时5ms
		}

		delay_ms(1000);	//1s 后,循环播放下一首音乐
	}
}

音符的表示

在以👆上的main.c程序中,变量**Note123_e song_notes[]**记录了需要播放的音乐的音符信息,本例所展示的音符序列,来自于以下简谱内容。这是《猪八戒背媳妇》的一个片段。

常规音符的表示

我们使用 notex 来表示一个常规音符(中音音符,无升降号)。例如我们要表示音符 1,我们需要写成 note1,同理对于音符 4,我们需要写成 note4

高音音符的表示

我们使用 notex_h 来表示一个高音音符,使用 notex_hh 来表示一个倍高音音符。例如我们要表示音符 高音3 ,我们需要写成 note3_h,同理,对于音符 倍高音3,我们需要写成 note3_hh

低音音符的表示

我们看了高音音符的表示方法,相信大家能猜出来如何表示低音音符了。对,就是在常规音符 notex 的基础上后加 _l ,或者 _ll 来表示。例如我们要表示音符 低音6 ,我们需要写成 note6_l,同理,对于 倍低音6,我们需要写成 note6_ll

半音音符的表示

我们在以上音符表示法的基础上,通过在音符后加一个下划线 _ 来表示音符升高半音。例如我们需要表示音符 2⁺ (此处右上角的 + 号表示一个 # 号),我们需要写成 note2_;再例如我们要表示音符 高音1⁺ (右上角的 + 表示一个 # 号),则我们需要写成 note1_h_ (音符结尾的下划线 _ 表示音符升高半音)。
注意: 由于音符 3 与音符 4 之间的音程只有一个半音,所以对于音符 3⁺ 我们直接使用 note4 表示。同理,对于音符 7⁺ 我们直接使用 note1_h 表示(这里需要理解一点乐理知识)。

有了以上的音符表示方法,我们就很容易的将一个简谱上的音符序列转换为stm32程序数据进行播放。

节拍的表示

在乐谱中,除了要表演的音符外,还有一个重要的信息就是节拍。即我们需要知道一个音符需要响多久。有的音符响的时间长,有的音符咱的时间短,如此错落有致,才能表演出动听音乐。
在以上程序中,我们在 float song_beats[] 中记录了每一个音符对应的节拍信息(时长)。

我们以 4分音符 为基准,表示一个节拍。

  • 如果一个音符没有下划线,没有后跟短划线,则这表示这个音符需要播放一个节拍的时长,我们用 1 表示;
  • 如果这个音符下面有 1 个下划线,则表示这个音符的播放时长要减半,我们就用 0.5 表示;
  • 如果这个音符下面有 2 个下划线,则表示这个音符的播放时长还要再关闭,我们用 0.25 表示;
  • 所以,音符下面有几个下划线,我们就把这个音符的节拍值(时长值)进行几次减半处理。
  • 如果一个音符的后面跟了 1 个短划线,我们理解这个音符的播放时长需要增加一个节拍时间,我们用 2 表示
  • 如果一个音符的后面跟了 2 个短划线,我们理解这个音符的播放时长需要再增加一个节拍时间,我们用 3 表示
  • 所以,如果音符的后面跟了几个短划线,我们就把这个音符的节拍值进行几次 +1 处理。
  • 特殊的,如果一个音符后面有一个小点,我们理解这个音符的播放时长需要增加个节拍的时间,我们需要将这个音符的节拍值+0.5 处理。
  • 节拍速度

    有了每个音符的节拍值,我们就知道这个音符需要播放多少个节拍的时长了。例如如果音符 note3 的节拍值是 1,我们知道这个音符 note3需要播放一个节拍的时长。但是一个节拍的时长是多少呢?是1s吗?还是100ms?还是1h?这个节拍值到绝对时间值的换算,就需要用到节拍速度这个信息了。

    👆上面的这个简谱中,♩=110这个就是拍速信息。它表示以(四分音符)为一拍,每分钟播放 110 拍。即一个拍的播放时间是 60/110 ≈ 0.5454s。再结合上面的每个音符的节拍值,我们就知道每个音符要播放多长时间了。

    注意(重要):如果你看到了 ♪=120这种拍速的表示,这表示以 (八分音符)为一拍,每分钟播放 120 拍。这可以等效描述为 “每分钟播放120个八分音符”,也可以等效描述为“每分钟播放60个四分音符”。我们表示拍速的时候,需要使用以 四分音符 为基准的拍速值(这个例子中,我们应该使用60表示拍速,而不应该使用120表示拍速)。

    在给定的本案例程序中,我们在 float song_beatSpd_bpm 中体现了节拍速度信息,这直接影响了音乐播放的快慢。

    调(diào)式

    在节拍速度的截图中,我们在左上角还可以看到一个 1=A 的信息;这就是简谱的调式信息,它说明在这个谱子中,中音 1 的声音频率对应于 note_a1。同理,如果你看到 1=C 这种调式,在程序中我们用 note_c1 表示。

    注意:在我们给的示例程序中,我们用的调式为 note_c1。即我们以 C 调演奏了这首乐曲。这与上图中显示的 1=A 是不一致的。读者可以自行调整。

    播放乐曲

    有了以上的信息后,我们就可以播放乐曲了。播放乐曲的原理是依次将音符对应频率的声音播放指定的时长,这样的一个声音序列,即是音乐。

    在主程序中,我们使用了 oled 屏来显示了所播放的音乐的标题信息。然后我们大致按以下次序循环播放了乐谱中的每一个音符:

  • 播放指针复位到0
  • 找到播放指针位置对应的音符对应的声音频率,然后播放这个频率的声音
  • 找到播放指针位置对应的音符的节拍值,计算出播放时间长度,然后延时这个长度的时间,维持声音的播放
  • 延时完成后,关闭这个频率的声音
  • 将播放指针后移一个音符
  • 延时 5ms (短暂停顿)
  • 从上面第2步开始播放下一个音符的声音,直到所有音符播放完成
  • 结束语

    在本案例中,我们讲解了如何将一个乐曲的简谱转换为一段stm32程序进行播放,也讲解了如何播放一首乐曲的原理。但是本案例所给的keil工程中还包含了大量的辅助程序没有讲解,这些程序为主程序的功能提供了强有力的支持(例如如何表示一个音符,一个音符如何换算成声音的频率)。强烈推荐读者朋友仔细研读。

    另外需要补充的是,在主程序中语句 **toneInit(TIM2, TIM_Channel_1);**说明了无源蜂鸣器的控制信号应该接到 TIM2 的通道1对应的引脚上。如果你调整使用了其它的定时器或者其它的通道,请将你的蜂鸣器的控制转接到对应的正确的 GPIO 引脚上。

    最后,欢迎大家一起讨论学习。

    作者:团圆吧

    物联沃分享整理
    物联沃-IOTWORD物联网 » stm32 无源蜂鸣器实验 播放音乐 猪八戒背媳妇

    发表回复