stm32108键C-B全调性_动态可视化乐谱钢琴

server/2025/2/23 11:00:54/

108键全调性钢琴

  • 一 基本介绍
    • 1 项目简介
    • 2 实现方式
    • 3 项目构成
  • 二 实现过程
    • 0 前置基本外设驱动
    • 1 声音控制
    • 2 乐谱录入&基础乐理
    • 3 点阵屏谱点动态刷新
    • 4 项目交互控制
    • 5 录入新曲子过程
  • 三 展示,与链接视频地址
    • 1 主要功能函数一览
    • 2 下载链接
    • 3 视频效果

一 基本介绍

本章为项目整体介绍

1 项目简介

本项目通过摇杆模块控制模式切换,TFTST7735S屏幕LED与点阵屏Max7219显示交互效果,喇叭输出声音。摇杆可控制8种模式,摇杆Z轴控制确定返回:

  • 播放音乐1-2-3
  • 控制声音:增加,减小,串口直接设定具体值
  • 摇杆音乐模式
  • 点阵谱点开关
    下为具体特点介绍:

(1)广泛音调支持
支持从1=CC#DD#EFF#GG#AA#B的所有音调,还可以设置大小调,或者用升降半调作为起始音,还能使用不同的八度区作为起始音等级,完美适配数字简谱,几乎可以演奏你熟悉的任何曲子。内部代码会根据设置的初始参数,算出该调性下其余一百零七个键的频率,不需要提前打表一堆数组,节省空间。

(2)精准还原声音
因为支持修改每个音符的升降半调升降度,并能精准控制音长节拍,所以播放音乐分辨率很高,音准极好,类似一台一百零八键全频率的电子钢琴。录入了3首音乐:含克罗地亚狂想曲梦回还

(3)使用简单
录一首谱子只需:一个纯数字一维乐谱数组,和一个一键设置调性的函数,用来配置新曲子。前者只要按照顺序和一定的规律对着数字简谱抄数字,后者更简单,根据简谱信息填两三个字母数字。不用懂很多,只要会看数字简谱。可播放任何单手数字简谱。

(4)点阵灯谱扫描
可以通过点阵屏显示不同音符节拍长度。一条扫描线从顶部扫描至底部,每个音符都有长度与不同位置,触碰到扫描线消失,类似音游。有两个注意点,一是如何让扫描线和声音同步变化,对时序必须严格控制。二是如何及时更新不同音符在点阵屏上的谱点,因为点阵屏长度有限,所以一定会遇到某个音符触碰底部,部分被截断的情况,需要及时在屏幕顶部恢复被截断的信息。

详细效果,见底部视频

项目涉及到许多常用外设,如用像是AD控制摇杆模块,脉宽调制PWM控制声音,串口收发数据,SPI,IIC`等基础知识。


2 实现方式

  • 音符 1-7:PWM控制喇叭发出不同声音。
  • 不同调性:了解乐理基础,根据数字简谱,提取参数,编写算法。
  • 乐谱录入:一个音符含: 度数/本身频率/时长/升降半调 4个综合信息,通过4位十进制表示。(第二章最后详细解释)
  • 动态乐谱显示:点阵屏驱动控制,精准到控制每一个点,从左到右x列对应以音符1-7,y轴表示音符长度,一行则表示最小节拍时长。
  • 交互:摇杆控制功能切换,TFT与LED显示内容变化。

3 项目构成

(1) 软件环境
上传程序:keil5
图片转c数组:img-lcd
字体编辑:PctoLcd
串口测试:串口助手

(2) 所需硬件

  • stm32F103C8T6
  • TFTst7735_RGB128*160——8引脚
  • 小喇叭 (或蜂鸣器,大喇叭)
  • 点阵屏MAX7219
  • 摇杆模块:XYZ3轴
  • LED灯RGB3色(非必须)

(3)接线

串口stm32
RXA9
TXA10
OLEDstm32
SCLB8
SDAB9
ST7735stm32
GND电源地
VCC3.3v电源
SCL接PA5
SDA接PA7
RES接PB0
DC接PB1
CS接PA4
BL空/3.3v
MAX7219点阵屏stm32
DinA6
CsB13
ClkB14
LEDstm32
RB10
GB11
BB13
摇杆模块stm32
XA0
YA1
ZC15
喇叭stm32
A2
GND

喇叭,TFT, 摇杆为必要外设。如不开灯谱,点阵屏亦可不接


二 实现过程

本章详解每一步如何实现

0 前置基本外设驱动

基础驱动实现,本文不在赘述。详细解释请跳转以下文章:

MAX7219 & IIC过程

TFTST7735 & SPI过程

串口收发数据 & UART协议

PWM脉宽调制

AD模数转换


1 声音控制

本节解释如何利用pwm发出不同频率,响度的声音

(1)声音特性
音调:指声音的高低,由物体振动的频率决定,频率越高,音调越高;频率越低,音调越低
响度:表示声音的大小或强弱,由振幅和人耳与声源的距离决定。振幅越大,距离越近,响度越大;振幅越小,距离越远,响度越小

声音的物理量
频率:单位时间内物体振动的次数,单位为赫兹(Hz),人类能听到的音频范围是20Hz~20kHz。
周期:物体完成一次振动所需的时间,单位为秒(s),与频率成反比。

(2)PWM控制声音
控制发出不同声音,就是是控制声音不同的音调响度,也就是频率声音大小。回想使用PWM控制舵机时,舵机频率是固定的50Hz,通过改变占空比控制舵机角度。而控制声音完全不同:

  • 第一:需要生成多种PWM频率
  • 第二:修改占空比,仅能改变声音大小,不改变音调频率。

故而在使用PWM播放音乐是,一定会面临需要经常变化PWM频率的问题,而我们知道:
PWM频率=(定时器频率/PSC)/ARR = 定时器频率/(PSC * ARR)

所以改变PWM频率,本质是不断修改PSC或ARR的值,为了简便二者改一个就可以。如:把pwm频率修改为:fre,则只要令
ARR=(定时器频率/PSC)/fre

PSC不变,故分子为常数。以72Mh时钟频率,PSC=72为例,修改PWM频率代码为:

/*
功能:运行中,修改PWM的频率
参数:修改后的频率 fre
公式:PWM频率=(定时器频率/PSC)/ARR = 定时器频率/(PSC * ARR)
*/
void PWM_SetFrequency(uint16_t fre) {int arr=72000000/72/fre;TIM2->ARR = arr;
}

正常人耳能听到声音的频率范围:大致在20Hz~20000Hz之间
108键钢琴的频率范围:是从最低音A0的27.5Hz到最高音C8的4186.01Hz

(3)代码核心部分
PWM.h

#ifndef __PWM_H
#define __PWM_H
/*
Tim2 4通道可控制4路PWM信号,使用通道3
PWM输出引脚:A2
*/
void PWM_Init(void);
void PWM_SetCompare3(uint16_t Compare); //改声音大小 1-999
void PWM_SetFrequency(uint16_t fre);    //改声音发声#endif

PWM.c 初始设定

#include "stm32f10x.h"                  // Device header
#include "PWM.h"
/*
功能:PWM初始化
*/
void PWM_Init(void)
{//仅举核心部分TIM_TimeBaseInitStructure.TIM_Period = 1000 - 1;		//初始给个1000TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;		//分频后为1MH
}
/*
功能:TIM2通道3-A2,占空比 (改变每个周期内高电平或低电平的持续时间)
参数:Compare要写入的CCR的值,范围:0~20000 ,即为ARR范围
*/
void PWM_SetCompare3(uint16_t Compare)
{TIM_SetCompare3(TIM2, Compare);		//设置CCR1的值,精确地设置高电平持续时间
}
/*
功能:运行中,修改PWM的频率
参数:计数周期,需要的频率
公式:PWM频率=(定时器频率/PSC)/ARR = 定时器频率/PSC * ARR
*/
void PWM_SetFrequency(uint16_t fre) {int arr=1000000.0/fre;TIM2->ARR = arr;
}

2 乐谱录入&基础乐理

数字简谱阅读基础必备:

基础乐理

(1)音符规律
由于音符间的音符十二平均律,相邻键(含黑键)频率乘以1.0594630943(二的十二次方根),频率大一度为2倍关系,低一度除以2。故只要确定1=?初始调性,即可计算出剩余108键的频率值,存入一个二维数组即可,大小为[9][12],代表0-8 九个度数区间的1-7音符频率、以及中间的黑键频率,对应钢琴9个区,每个区为12个键。

(2)大小调
大小调简单理解为就是一种1-7不同位置按法,大小调频率表相同,不需要重新计算。区分大小调只需在代码种只需要加入2个映射数组即可(20种弹法也没有问题)

计算频率参考全全局变量值:

//------------------------计算频率参数-------------------
/*
音符数组-标准钢琴108键
1 8个度区,升降度直接改一维下标
2 每个度区,含cdefgab以及降半调共12个全黑白键,设计一映射数组1-7到第二维度,降半调+10计算
3 乐谱4位十进制:半调(0-2) 音调:1-7  音度:0-8 (大1则高8度) 时长:1-2-4 拍数
*///计算频率参考数组
uint16_t note[9][12]={//C C# D  D# E  F  F#  G G# A  A#  B{16,17,18,19,20,21,23,24,25,27,29,30}, //0 八度区 {32,34,36,38,41,43,46,48,51,55,58,61}, //1{65,69,73,77,82,87,92,97,103,110,116,123},//2{130,138,146,155,164,174,785,196,207,220,233,246}, //3{261,277,293,311,329,349,369,392,415,440,466,493}, //4{523,554,587,622,659,698,739,783,830,880,932,987}, //5{1046,1108,1174,1244,1318,1396,1480,1568,1661,1760,1864,1975},//6{2093,2217,2349,2489,2637,2793,2960,3136,3322,3520,3729,3951},//7{4186},//8  C8
};
//在note中找值的映射数组
uint8_t book_note[8]={0,0,2,4,5,7,9,11};//计算107个频率生成数组,存入此处,一曲一更,覆盖。
uint16_t note2[9][12]={0};//大调映射 0-0 1-0 2-2 3-4 4-5 5-7 6-9 7-11(下一个1略) 全全半全全全(半),+2 +2 +1 +2 +2 +2 +(1)
uint8_t major_scale[8]={0,0,2,4,5,7,9,11};//
//小调映射 0-0 1-0 2-2 3-3 4-5 5-7 6-8 7-10(下一个1略) 全半全全半全(全)+2 +1 +2 +2 +1 +2 +(2)
uint8_t minor_key[8]={0,0,2,3,5,7,8,10};

生成对应调性如下:另外107键的频率过程:

/*
功能:制造专属歌曲的频率数组
参数Scale:音调 C——B,c-b 大小调
参数dp: 0-1-2  降半调,无,升半调
参数degree:度数0-8
解释:适配 'do'=(C-B,c-b,#/b,0-8)全音调,108键全区每次播放歌曲前,调用此函数,计算出合适的频率并保存例:make_scale(C,1,4)  do=C4make_scale(D,2,5)  do=D#5
原理:根据一个参考数组,算出当前调性下的初始'do'的频率,往后排11个键,就是该度下所有键上下跨度,乘2或除2就可以得出。
*/
void make_scale(int Scale,int dp,int degree){//大小调do频率固定int t;double ans=1.0594630943;double tem=0;if(Scale=='a'||Scale=='b') //定初始调位置1-7t=6+Scale-'a';else t=Scale-'c'+1;if(Scale=='A'||Scale=='B')t=6+Scale-'A';else t=Scale-'C'+1;t=book_note[t]+dp-1; //定do的位置,dp-1 -1 0 1,计算升降调tem=note[degree][t];//不管大小调,初始do频率相同,变化都的是2-7位置,也就是2个映射数组//degree度,12键音note2[degree][0]=tem;for(int i=1;i<12;i++){tem*=ans; //保留进度note2[degree][i]=round(tem); //四舍五入}//degreee-1~0for(int i=degree-1;i>=0;i--)for(int j=0;j<12;j++)note2[i][j]=note2[i+1][j]/2;//degreee+1~8for(int i=degree+1;i<=8;i++)for(int j=0;j<12;j++)note2[i][j]=note2[i-1][j]*2;
}

(3)曲谱录入
由于本项目对于音准的高要求,所以简谱中每个音符包含4个信息:是否升降半调本身音调是否升降度节拍。不需要引入字母,使用4位数字记录,个十百千位的数字记录信息,做法类似于寄存器2进制写入数据。

  • 千:0 1 2 降/平/升 半调 (升降半调,即#/b,输出数组后一位或前一位元素)
  • 百:频率 1-7
  • 十:度:0-8度 (升度频率乘2)
  • 个:记录节拍 1 2 4 (根据曲速可计算一拍时长t,t除以4/(1、2、4)便是该音符发声时长,为了方便记忆,否则设为421也可以,本质是得出有几个最小一拍
  • 曲速:如曲速为112,则一拍时长为:60/112 s

如实际谱子:

/*
曲1:Dream of cherry tree
1=Db 大调 beat=68 4/4   68太慢了,提高到80
*/
double beat1=60.0/80*1000;  //DC曲速  原曲:60.0/68
uint16_t song_DC[]={1351,1251,1352,1552,1351,1351,1051,1041,1251,1351,1251,1352,1552,1161,1161,1061,1162,1752,1652,1552,1351,1251,1352,1552,1351,1351,1051,1542,1642,1152,1252,1351,1251,1352,1552,1361,1361,1061,1262,1162,1752,1552,1651,1051,1551,1051,1451,1551,1652,1552,1351,1251,1051,1152,1742,1642,1152,1741,1041,1041,1041,1552,1652,1752,1162,1651,1652,1352,1451,1551,1652,1452,1351,1251,1042,1352,1742,1252,1151,1152,1644,1744,1152,1742,1152,1252,1352,1252,1251,1351,1251,1352,1552,1361,1042,1262,1162,1752,1652,1552,1551,1351,1251,1352,1552,1361,1042,1462,1362,1262,1162,1752,1751,1042,1752,1162,1262,1161,1162,1552,1651,1652,1452,1552,1452,1352,1252,1151,1051,1051,1051
};

(4)播放声音
遍历乐谱数组,依次播放每个音符即可。下为播放一个音符的函数:

/*
功能:按照时长演奏一个音符(不含点阵屏变化)
参数ans:复合音符
参数btspeed:一拍的曲速单位:ms
参数scale:大调1   小调0 
解释:根据音符信息,演奏音符,oled显示参数无灯模式效果精准,音长更及时
*/
void unlight_Sound(int ans,int btspeed,int scale){int half=ans/1000;  //0 1 2  降半音b   正常  升半音#int msc=ans/100%10; //1-7 ,不含0 8int degree=ans/10%10; //0-8 度int tm=ans%10;  //节拍 1 2 4 int fre;//<1> fre为获取二维数组下标,1-7与大小调映射if(scale) fre=major_scale[msc];  //大小调-判断else  fre=minor_key[msc];if(half-1==1) fre++;          //<2> 判断是否升降半调else if(half-1==-1) fre--;    // -1 0 1 升/降半调,前一个或后一个元素if(msc==0) PWM_SetCompare3(0);  //<3>修改声音大小,空音设占空比0else PWM_SetCompare3(voice);PWM_SetFrequency(note[degree][fre]);  //<4>设置声音频率发声tm=btspeed/tm;  //<5>控制发声时长Delay_ms(tm);  //OLDED参数显示OLED_ShowString(3,7,"degree:"); OLED_ShowNum(3,14,degree,1);OLED_ShowString(4,1,"fre:");OLED_ShowNum(4,5,note[degree][fre],4);OLED_ShowString(4,10,"msc:");OLED_ShowNum(4,14,msc,1);//PWM_SetCompare3(0);//每个音符后额外停顿,曲谱精准时,无需设置//Delay_ms(5);
}

此函数仅用于播放声音,不控制点阵屏,注意与下面函数区分


3 点阵屏谱点动态刷新

点阵屏8*8XY轴代表信息:
X:1-7列对应音符1-7
Y:每一行代表音符发声时长,一行为最小节拍,通过60/曲速计算得出,Delay进行控制
扫描线会从上至下扫描,碰到音符,使其消失,和乐谱中每个音符的时长对应同步。此算法设计包含以下部分:

  • 扫描线变化与乐谱的声音长度同步(以Delay延时最小节拍为基础)
  • 当扫描线达到顶端-立刻向后更新一屏的音符信息(非同步,而是某时刻更新)
  • 预处理曲谱中每一个音符的显示信息(代码计算处理,人无需计算)
  • 恢复被截断的底部音符保证正确性(准确性)

(1)预处理乐谱每个音符的点阵信息
预处理每个音符在点阵屏上出现的起始行结束行位置,存入标记数组msc_sort中。用于后续输出某音符在点阵屏上的具体位置,注意截断:

/*
功能: 并预处理曲铺每个音符灯点位置信息
参数:  1-3,播放歌曲前,先调用此函数
解释:msc_sort数组覆盖,每次播放前都需调用获取每个音符在点阵屏上 出现的起始行与结束行位置,存入msc_sort*起始不可超过8,结尾可大于等于8,会在动态更新点阵屏时利用结尾特性,判断触底截断音符
*/
void load_msc(int num){int tem,mucis_num; if(num==1){mucis_num=sizeof(song_DC)/sizeof(song_DC[0]); //音符数msc_sort[0][0]=1;msc_sort[0][1]=4/(song_DC[0]%10);for(int i=1;i<mucis_num;i++)   //每一个的值,依据前一个结尾计算{tem=msc_sort[i-1][1];if(tem>=8) tem=tem%8+1;else  tem+=1;int r=4/(song_DC[i]%10);    //当前音符所占1/4节拍数,就是需要几个点点亮msc_sort[i][0]=tem;        //起始必须小于等于8msc_sort[i][1]=tem+r-1;    //结尾可以大于等于8}}if(num==2){//同上}if(num==3){//同上}
}

(2)显示单个音符谱点函数
欲显示整个乐谱,需先从显示一个音符开始:

/*
功能:显示一个音符的位置
参数k:曲库中下标,音符
参数ans:复合音符值
介 绍:在预处理数组中记录了开始点与结束点,此处显示时超过底部不予显示在动态更新乐谱时,在对确实的另外半截手动设置填补,否则会覆盖第一行
*/
void note_show(int k,int ans){int t=4/(ans%10);   //音符长度int x=ans/100%10;   //音符0-7int y=msc_sort[k][0];  //音符起始行,不会超过8//向下显示t个音符,超过不显示,所以不用msc_sort[1]for(int i=0;i<t;i++){Max7219_ShowGraph(y,x,x);y++;if(y>8) break; //先显示一次,在结束}
}

需注意,由于音符触底超长部分,需要截断,留在下一次显示,所以超过第8行不显示。补充截断部分已在对应处单独编写代码。


(3)扫描线、声音、音符点、同步方式(核心)
此处与上述unlight_Sound数组区别是,添加的while循环部分。单纯播放声音,只需要PWM写入频率然后延时即可。但添加乐谱后,由于需要控制扫描线音符声长同步,以2个最小节拍长的音符为例,则实际过程为:

PWM发声->扫描线1出现->Delay最小节拍1->扫描线1消失->扫描线2出现->Delay最小节拍2->扫描线2消失。

这样可以做出声音与点阵屏同步更新的效果,以最小延时为基础,人眼是察觉不到先后顺序的,形成声画同步效果。其次是,每个音符的谱点并非是:只有播放到该音符,才更新一个而是扫描线出现在顶部时,一次提前更新一屏幕的内容,这样才有提前读谱的效果(类似全民k歌),而判段什么时候到达底部,以及如何恢复被截断的音符部分:通过预处理的谱点数组msc_sort实现。

/*
功能:按照时长演奏一个音符,扫描线对应同步变化
参数1:音符编号
参数3:复合数组 (需要预处理点阵,所以要传递数组)
参数3:beat节拍速度
参数4:大调1,小调0
解释:扫描线于声音同步,乐谱点是在每次扫描到第一行时更新满屏。扫描线:与声音同步,每个最小节拍间隔,点阵屏都要亮灭一次乐谱点阵显示:每次到第一行时,刷新一整个点阵屏的音符点,向后遍历遇到底部停止难点在于补全上一次末尾超长截断的灯点
*/
void light_Sound(int k,uint16_t *ans,int btspeed,int scale){int half=ans[k]/1000;  //0 1 2  降半音b   正常  升半音#int msc=ans[k]/100%10; //0-7int degree=ans[k]/10%10; //0-8 度int tm=4/(ans[k]%10);  //几个1/4节拍  1 2 4 int fre;								//<1> fre为获取二维数组下标,1-7与大小调映射if(scale) fre=major_scale[msc];  //大小调-判断else  fre=minor_key[msc];if(half-1==1) fre++;  	//<2> 是否升/降半调,-1 + 1前一个或后一个元素else if(half-1==-1) fre--; if(msc==0) PWM_SetCompare3(0);  //<3> 音量设置,注意0不发声else  PWM_SetCompare3(voice);PWM_SetFrequency(note[degree][fre]);  //<4>发声int y=msc_sort[k][0];    //扫描线位置 1-8行while(tm--)              //tm个最小节拍{if(y==1){              //<5> 扫描到第一行,立刻向后更新一屏的乐谱点阵for(int i=0;i<8;i++){if(i==0){  //当前字符是否为被截断字符,并恢复截断长度if(msc_sort[k][1]>=8){    //避免连续出现一个长度,无法到达下一个,从而断屏for(int n=1;n<=msc_sort[i][1]-8;n++)Max7219_ShowGraph(n,msc,msc);}elsenote_show(k,ans[k]);}else{note_show(k+i,ans[k+i]);    //显示单个字符的点阵if(msc_sort[k+i][1]>=8)     //到底,结束,超长部分不显示break;}}}Max7219_ShowGraph(y,1,8);  //<6>扫描线随 最小节拍更新Delay_ms(btspeed/4.0);     //发音,同时亮灯               Max7219_Clearline(y);y++;     //扫描线位置更新,最多到9if(y>8) y=y%8;   //超过8,则%8为1-7,不能在加1,会跳过空行}OLED_ShowString(3,7,"degree:");OLED_ShowNum(3,14,degree,1);OLED_ShowString(4,1,"fre:");OLED_ShowNum(4,5,note[degree][fre],4);OLED_ShowString(4,10,"msc:");OLED_ShowNum(4,14,msc,1);//PWM_SetCompare3(0);//每个音符间额外停顿//Delay_ms(5);
}

此处为 模拟过程,想要直接理解有难度,最好自己去编写尝试


(4)完整播放乐曲函数
最终封装遍历音乐的函数,可设置是否打开点阵屏模式与大小调选择,由于基于最小延时节拍,同步点阵与声音,所以会占据部分cpu性能,若只需播放音乐的话,可设置关闭。

/*
功能:播放音乐
参数1:曲目1-3
参数2:1 灯光  0 非灯光
参数3:1 大调  0 小调
解释:可选择灯光模式
*/
void Play_msc(int num,int mode,int scale){//可调控曲速if(num==1){int r=sizeof(song_DC)/sizeof(song_DC[0]);if(mode){for(int i=0;i<r;i++)  //有动态灯谱light_Sound(i,song_DC,beat1,scale);     //<2>修改2,大小调,1为大PWM_SetCompare3(0);//播放完静音}else{for(int i=0;i<r;i++)unlight_Sound(song_DC[i],beat1,scale);PWM_SetCompare3(0);}}if(num==2){//同上}if(num==3){//同上}
}

变速: 如一首曲子需要多个曲速,可以在对应for循环内部中判断,当到达某个音符时 ,改变beat值接口,加几个if语句即可解决。


4 项目交互控制

本质为利用摇杆8个模拟值区间(上下左右与斜对角),控制8个不同模式,利用不同flag状态控制界面

rocker_music函数效果为,根据AD采集摇杆信息,判断摇杆位置0-8值,来播放不同的音调1-7
TFT_update效果为:根据不同数字,显示不同的文字内容
AD摇杆模块的实现,过于基础不再描述。包括设置全局变量声音、串口通信获取信息、TF图片文字显示。

/*接线
1 串口连接:RX-A9 TX-A10
2 OLED连接:SCL-B8 SDA-B9
3 TFTST7735:GND   电源地VCC   3.3v电源SCL   接PA5SDA   接PA7RES   接PB0DC    接PB1CS    接PA4 BL		空/3.3v
4 MAX7219点阵屏: Din-A6 Cs-B13  Clk-B14
5 LEd_RGB: B10 B11 A3
6 摇杆:XYZ: x-A0 y-A1 z-C15 
7 喇叭:A2 (另一根线接GND)
*/
#include "stm32f10x.h"  // USE_STDPERIPH_DRIVER
#include "delay.h"      //--no-multibyte-chars
#include "OLED.h"
#include "Serial.h"#include "LED.h"    
#include "KEY.h"  
#include "AD.h"
#include "PWM.h"#include "St7735tft.h" //TFT
#include "Max7219.h"   //点阵屏
#include "playmusic.h" //主要功能集成函数int main(void)
{AD_Init();PWM_Init();LED_Init();KEY_Init();TFT_Init();Max7219_InitDisplay();OLED_Init();Serial_Init();TFT_UI();  //显示自定义界面OLED_ShowString(1,1,"SKY_Music");int z,m1,mode=0;  //z轴状态,m1与mode用来记录摇杆位置int falg_rocker=0;//摇杆标记/*C-B调全升降调打表,以do=4度为例,最大支持0-8度区间for(int i='A';i<='G';i++){for(int j=0;j<=2;j++)make_scale(i,j,4);Serial_Printf("\r\n");}*/while (1){//可随时串口更新音量if(Serial_RXString())set_voice();z=KEY_Getnum(); //z轴状态OLED_ShowString(3,1,"Z:");OLED_ShowNum(3,3,z,1);//自由编曲独占模式if(falg_rocker){if(z==1) falg_rocker=0;rocker_music();continue;}//记录摇杆m1=Get_rocker();  //摇杆一松手会归0,所以需要2个值记录,无法及时TFT_update(m1);if(m1!=0) mode=m1; //记录变化的操作/*@@@@@串口测试,获取值Serial_Printf("z=%d m1=%d mode=%d\r\n",z,m1,mode); *///调用功能if(z&&mode==1){  			           //上:DC Dream of cherry treeload_msc(1);//载入歌曲make_scale('D',DOWN,4);       //调性D,b降半调,度数4  1=Db4Play_msc(1,light_Turn,MAJOR); //曲目编号,灯光模式,MAJOR大调}else if(z&&mode==2){		          //左上:add声音lower_voice();TFT_update(2);}else if(z&&mode==3){		        //左:克罗迪亚load_msc(2);make_scale('E',DOWN,4);       //1=Eb4,小调Play_msc(2,light_Turn,MAJOR);       }else if(z&&mode==4){		         //左下:low声音upper_voice();TFT_update(4);}else if(z&&mode==5){		         //下:梦回还load_msc(3);Delay_ms(400);//曲目长,更新下数组make_scale('A',NORMAL,4);  //A4  不降调Play_msc(3,light_Turn,MAJOR);}else if(z&&mode==6){		         //下右:空}else if(z&&mode==7){          //右: 自由摇滚-独占,rocker音乐falg_rocker=1;   		           }else if(z&&mode==8){            //右上:灯光模式light_Turn=!light_Turn;TFT_update(8);LED_On(1);Delay_ms(100);LED_On(2);Delay_ms(150);LED_On(3);Delay_ms(200);LED_Off(1);LED_Off(2);LED_Off(3);}}
}//摇杆的按钮挂个中断,及时反应
void EXTI15_10_IRQHandler(void){           //(5)创建中断函数//startup_stm32f0x_md.s找——->Table Mapped,DCD栏以EXTI_IRQHandler结尾//判断是不是15通道,exti.h————>EXTI_GetITStatus与EXTI_ClearITPendingBit函数if(EXTI_GetITStatus(EXTI15_10_IRQn)==RESET){EXTI_ClearITPendingBit(EXTI_Line15);//清除标志位Serial_Printf("Z按钮进入中断\r\n");}
}

5 录入新曲子过程

(1)录入乐谱
进入Song.h文件:

  • 乐谱数组覆盖3首中随机一首的信息即可。
  • 修改对应beat曲速,数字简谱会有曲速说明

以一小节为例,4/4拍:
在这里插入图片描述
录谱过程:
在这里插入图片描述

实际录谱时,使用语音识别输入,与excel批量处理数字数据,很快捷,因为规律十分的简单


(2)修改带调性函数
进入main.c文件,修改对应调性大小调参数即可:

//根据编号信息修改
else if(z&&mode==3){load_msc(2);make_scale('E',DOWN,4); //1=Eb4, 改A-G,DOWN/NORMAL/RISE 降平升半调,初始度数0-8(一般为4)Play_msc(2,light_Turn,MAJOR);  //改大小调,MAJOR或MIRROR  }

#为升:使用RISE
b为降:用DOWN
否则:NORMAL
A-G大小写通用
大调:MAJOR
小调:MIRROR


三 展示,与链接视频地址

本章内容为:主要功能函数展示与实现效果,附上下载链接

1 主要功能函数一览

#ifndef __PLAYMUSIC_H
#define __PLAYMUSIC_H/*song中引用文件:
note[9][12]   频率文件
book_note[8]  映射
song_DC[]    曲库1
song_Croatia[] 曲库2
song_DreamBack[] 曲库3
beat1     曲1节拍
beat2 
beat3
voice        声音 
*/#define RISE 2  //升半调
#define NORMAL 1  
#define DOWN 0    //降半调
#define MAJOR 1 //大调
#define MINOR 0 //小调//playmusic定义
extern int light_Turn; //开关灯光模式
extern uint16_t msc_sort[800][2]; //预处理:每个音符在点阵屏初始位置信息//----------------功能函数-----------------------
//音乐播放
void set_voice(void);     //串口准确更新声音1-999
void lower_voice(void);    //按一下,+200
void upper_voice(void);void load_msc(int num);   //载入曲谱灯铺信息
void make_scale(int Scale,int dp,int degree);//制作不同曲目调108键调性表
void rocker_music(void);//摇杆音乐
void Play_msc(int num,int mode,int scale);  //曲目编号,灯光模式0-1//TFT
void TFT_UI(void);
void TFT_update(int k);//-------------------内部函数-----------------------
//灯
void unlight_Sound(int ans,int btspeed,int scale); //音符无灯
void light_Sound(int k,uint16_t *ans,int btspeed,int scale);  //音符带灯,动态更新乐谱
void note_show(int k,int ans); //显示单个音符的点阵(超过底部部分不显示)#endif

2 下载链接

点击下载源文件:

与君共勉


3 视频效果

stm32自制可视化乐谱108键钢琴,小白轻松奏神曲



http://www.ppmy.cn/server/170107.html

相关文章

Unity中点乘和叉乘对于我们来说的作用是什么?

在 Unity 中&#xff0c;点乘&#xff08;Dot Product&#xff09;和叉乘&#xff08;Cross Product&#xff09;是向量运算里非常重要的操作&#xff0c;在游戏开发、动画制作、物理模拟等多个方面都有着广泛的应用。 目录 1 点乘 2 叉乘 1 点乘 通过点乘公式 &#xff0c…

DeepSeek行业应用实践报告-智灵动力【112页PPT全】

DeepSeek&#xff08;深度搜索&#xff09;近期引发广泛关注并成为众多企业/开发者争相接入的现象&#xff0c;主要源于其在技术突破、市场需求适配性及生态建设等方面的综合优势。以下是关键原因分析&#xff1a; 一、技术核心优势 开源与低成本 DeepSeek基于开源架构&#xf…

分布式与集群,二者区别是什么?

??分布式 分布式系统是由多个独立的计算机节点组成的系统&#xff0c;这些节点通过网络协作完成任务。每个节点都有自己的独立计算能力和存储能力&#xff0c;可以独立运行。分布式系统的目标是提高系统的可靠性、可扩展性和性能。 分布式服务包含的技术和理论 负载均衡&am…

Jmeter HTTP代理服务器录制压力脚本

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 从loadrunner到jmeter&#xff0c;录制压力测试脚本好像都只支持IE&#xff0c;近来才知道jmeter还有自带的录制脚本元件&#xff0c;且支持IE、Chrome及Firefox等…

使用 DeepSeek 生成流程图、甘特图与思维导图:结合 Typora 和 XMind 的高效工作流

在现代工作与学习中&#xff0c;可视化工具如流程图、甘特图和思维导图能够极大地提升信息整理与表达的效率。本文将详细介绍如何使用 DeepSeek 生成 Mermaid 文本&#xff0c;结合 Typora 快速生成流程图和甘特图&#xff0c;并通过 Markdown 格式生成思维导图&#xff0c;最终…

嵌入式八股,全局变量和局部变量的区别

1. 作用域 全局变量&#xff1a; 定义位置&#xff1a;在所有函数之外定义。 作用域&#xff1a;全局变量在&#xff08;程序块&#xff09;整个程序范围内都可访问&#xff0c;包括所有函数。 局部变量&#xff1a; 定义位置&#xff1a;在函数内部或代码块内部定义。 作…

rsync使用详解

rsync的使用场景 rsync主要用于备份和镜像。具有速度快、避免复制相同内容和支持符号链接的优点。 安装rsync yum install rsync rsync -a参数的作用 rsync 是一个功能强大的文件同步工具&#xff0c;-a 参数是其常用选项之一&#xff0c;主要用于归档模式&#xff08;arch…

Qt ModbusTCP和ModBusRTU读写数据

文章目录 ModbusTCP和ModBusRTU 的区别ModbusTCP添加模块ModBus读写 ModBusRTU添加模块 ModbusTCP和ModBusRTU 的区别 Modbus RTU 和 Modbus TCP 是 Modbus 协议的两种不同实现方式&#xff0c;主要区别如下&#xff1a; 通信介质 Modbus RTU&#xff1a;基于串行通信&#xf…