硬件(esp32),服务器(python),前端,三端联调的电子琴项目,可以实现多种乐器奏乐。

关于本次项目的简略介绍:

大体分为硬件端和软件端,硬件端包括:esp32在面包板上的连接,画pcb电路图,焊接调试pcb电路,最后用三维设计外壳。软件端包括:用python编写的后端服务器,页面展示的客户端。(本项目要用到有关乐理知识)

我们的项目总的流程图

 文字描述:esp32硬件先连接WiFi,蓝灯亮了表示连接成功,不亮就用蜂鸣器发声,亮的话,esp32的琴键摁下将对应摁键通过udp通讯发送到服务端,服务端收到udp信息 可以利用pygame发声,也可以通过服务端和前端的websocket连接,将信息发送到前端,前端收到信息,去实现对应的琴键变颜色,记录简谱。

还可以通过前端的琴键摁下,让服务端的pygame发声,并变颜色和记录简谱。

拓展部分:1.作弊模式:硬件摁下一个摁键,不管硬件摁下哪个按键,都发出前端已经规定好的音谱。

软件端要达到的效果如下:

一 后端(thonny)

        1.使esp32发出不同的声音

        2.esp32发声(奏乐)

        2.录制esp32发音,判断摁键摁下的长短

        3.利用pygame发声

        4.用udp连接使服务器和硬件建立联系,硬件摁下琴键服务端收到udp信息,发出对应的音调

        5.esp32的WiFi连接

二 前端(vscode)

        1.在页面绘制UI琴键

        2.构建电子琴结构

        3.服务端和前端建立websocket连接

        4.硬件发送udp向服务端,服务端收到信息通过websocket连接发送到前端

以下是拓展部分:

        1.开启作弊模式

        2.切换上一首下一首,清除页面前一个音符,清空页面全部音符

        3.播放音乐

硬件端要达到的效果如下:

        1.esp32在面包板上的连接

        2.画pcb电路图

        3.pcb电路板的焊接

        4.三维模型的设计打印

最终三维打印,前端页面优化以及乐器多重奏:

        1.前端优化

        2.三维设计,pcb焊接,锂电池的供电

        3.硬件的调试(按定义好的音调奏乐)

                        “坚持就是胜利”

------------------------------------------------------------------------------

在动手做之前我们应该了解基本乐理知识:音乐基础乐理知识大全 | 乐理知识 - 知乎 (zhihu.com)

在发音之前我们一个了解蜂鸣器有关知识:蜂鸣器原理 - 单片机教程 - C语言网 (dotcpp.com)

ESP32 MicroPython上手指南 — MicroPython 1.14 文档 (01studio.org)esp32官网:ESP32 MicroPython上手指南 — MicroPython 1.14 文档 (01studio.org)

了解面包板:(4条消息) 面包板使用简介_countofdane的博客-CSDN博客_面包板的详细使用方法

用到的硬件有esp32  剥线钳 导线 蜂鸣器(下图是esp32)

 "万事俱备只欠东风  --------------------------------"

        首先我们需要先将esp32在面包板上的连线完成

 完成连线过后我们可以去实现功能了

①当面包板上的琴键摁出发出不同音调并且亮蓝灯

        代码如下:

                        

from machine import TouchPad, Pin,PWM  
from time import sleep

LED = Pin(2,Pin.OUT)

pwm0 = PWM(Pin(23))
freq_val=5
duty_val=0
pwm0.freq(freq_val)
pwm0.duty(duty_val)


PINn = (32,33,27,13,12,14,15,4)#元组

TPx = [TouchPad(Pin(i)) for i in PINn]#TPx 所有的触摸管脚定义

Tone2 = (#对应管脚要演奏声音的频率
    523,
    586,
    658,
    697,
    783,
    879,
    987,
    1045,
    )

def dzq():
    global freq_val,duty_val
    '电子琴子程序'
    for ton,tp in zip(Tone2,TPx):
        '遍历所有的触摸按钮'
        if tp.read()<200:
            freq_val=ton
            duty_val = 1024//2
            LED.on()
            break
    else:
        #'如果运行完所有的循环,没有break,就执行这个else'
        freq_val = 10
        duty_val = 0
        LED.off()
        
    freq1 = pwm0.freq()#读取当前频率
    
#     pwm0.freq(freq_val)
#     sleep(0.01)
#     pwm0.duty(duty_val)
    
    if freq1 != freq_val:
        '只有频率被修改的时候,才去重新配置频率和占空比'
        pwm0.freq(freq_val)
        sleep(0.01)#需要一定时间的延时,否则的话,同时修改评论和占空比会导致占空比修改失败
        pwm0.duty(duty_val)
        
#         while pwm0.duty() != duty_val:
#             pwm0.duty(duty_val)
#         print(pwm0.freq(),pwm0.duty())
    
while True:
    dzq()
    sleep(0.01)

------------------------------------------------------------------------

②实现录制播放并且记录摁下时间长短:

import time #引入时间类
from machine import TouchPad, Pin, PWM #引入触摸管脚
import threading

switch = TouchPad(Pin(32))
switch_mode = 0

PINn = (33,27,13,12,14,15,4)#元组
TPx = [TouchPad(Pin(i)) for i in PINn]#TPx 所有的触摸管脚定义

LED = Pin(2, Pin.OUT)

pwm0 = PWM(Pin(26))
time.sleep(0.5)
pwm0.duty(0)
time.sleep(0.01)
pwm0.freq(1)

Tone = (#对应管脚要演奏声音的频率
    523,
    586,
    658,
    697,
    783,
    879,
    987,
    )

my_rhythm = [] #创建节拍空数组
my_tones = [] #创建音调空数组

release=0 #松开状态
press=1 #按下状态
key_state = [release]*8 #按键状态缓存变量
ALLkey_state = release
start = time.ticks_ms() #记录开始的时间戳
end = 0 #记录结束的时间戳
time_diff = 0 #开始和结束的时间差


def Record_time():
    global start,end,time_diff
    end = time.ticks_ms() #记录这一次按键变化结束的时间戳
    time_diff = time.ticks_diff(end, start) #根据这一次按键变化的开始和结束时间戳,计算出按键变化的时间差
    my_rhythm.append(time_diff/1000) #将时间差存入数组末尾
#     print(my_rhythm) #打印数组
    #-----------------------------------------------------
    start = time.ticks_ms() #记录下一次按键变化开始的时间戳

def playtone(frequency):
    pwm0.duty(512)
    time.sleep(0.01)
    pwm0.freq(frequency)

def bequiet():
    pwm0.duty(0)
    time.sleep(0.01)
    pwm0.freq(1)

def playsong():
    global switch_mode
    for i in range(len(my_rhythm)):
        if (my_tones[i] == 0 ):
            bequiet()
        else:
            playtone(my_tones[i])
        time.sleep(my_rhythm[i])
    bequiet()
    switch_mode=0
    print('music_OK')

my_music = threading.Thread(target=playsong)


def switchkey():
    global switch_mode,start
    while True:
        if switch.read()<200:
            if switch_mode==0:
                my_rhythm.clear()
                my_tones.clear()
                switch_mode=1
                start = time.ticks_ms() #记录开始的时间戳
                my_tones.append(0) #第一个项为空拍,与节拍数组格式对应
                LED.on()
            elif switch_mode==1:
                Record_time()
                print(my_tones)
                print(my_rhythm)
                switch_mode=2
                LED.off()
                my_music.start()
            while switch.read()<200:
                time.sleep(0.01)
        
sw_key = threading.Thread(target=switchkey)
sw_key.start()

while True:
    for i in range(7): #循环7次,读取7个按键状态
        if TPx[i].read()<200: #读取按键电容值,判断按键是否按下
            if key_state[i] != press: #如果按键状态缓存变量 非 此按键键值,表示按键发生了改变
                key_state[i] = press #将按键状态缓存变量 置为 此按键键值
                if switch_mode==1:
                    Record_time() #记录按键改变的时间差
                    my_tones.append(Tone[i]) #记录当前按键的音调
                playtone(Tone[i]) #发出对应频率的音调
            ALLkey_state = press #表示有按键按下
        else:
            key_state[i] = release #将当前按键状态置为松开
        
    if press not in key_state:
        if ALLkey_state != release : #全部松开状态下 判断之前是否有按键按下
            ALLkey_state = release #将按键状态置为全部松开状态
            if switch_mode==1:
                Record_time() #记录按键改变的时间差
                my_tones.append(0) #记录当前空拍音调
            bequiet() #不发声
              
    time.sleep(0.1) #延时

③利用pygame发声   a~z摁下都可以发声,记录并打印摁下时间:

import pygame.midi
import pygame
import time

# 初始化设置
volume = 127  # 音量 0-127
pygame.init()  # 初始化PYgame
windowSurface = pygame.display.set_mode((800, 600))  # 建立窗口
device = 0  # device number in win10 laptop
instrument = 0  # 乐器 http://www.ccarh.org/courses/253/handout/gminstruments/
# initize Pygame MIDI ----------------------------------------------------------
pygame.midi.init()  # PYGAMEMIDI库的初始化
# 初始化设置结束

screen = pygame.display.set_mode((400,400))

# 设置窗口的标题,即游戏名称
pygame.display.set_caption('pygame 钢琴')

# 引入字体类型
f = pygame.font.Font('C:/Windows/Fonts/simhei.ttf',135)
# 生成文本信息,第一个参数文本内容;第二个参数,字体是平滑;
# 第三个参数,RGB模式的字体颜色;第四个参数,RGB模式字体背景颜色;
text = f.render("Zhang",True,(255,244,255),(31,56,99))
#获得显示对象的rect区域坐标
textRect =text.get_rect()
# 设置显示对象居中
textRect.center = (200,200)
# 将准备好的文本信息,绘制到主屏幕 Screen 上。
screen.blit(text,textRect)

# 固定代码段,实现点击"X"号退出界面的功能,几乎所有的pygame都会使用该段代码
Tone = {  # 音调字典,不全,需要大家完善。从C1-C5都完善起来
   'A0':21,'A#0':22,'B0':23,
   
   'C1':24,'C#1':25,'D1':26,'D#1':27,'E1':28,'F1':29,'F#1':30,'G1':31,'G#1':32,'A1':33,'A#1':34,'B1':35,
   
   'C2':36,'C#2':37,'D2':38,'D#2':39,'E2':40,'F2':41,'F#2':42,'G2':43,'G#2':44,'A2':45,'A#2':46,'B2':47,
   
   'C3':48,'C#3':49,'D3':50,'D#3':51,'E3':52,'F3':53,'F#3':54,'G3':55,'G#3':56,'A3':57,'A#3':58,'B3':59,
   
   'C4':60,'C#4':61,'D4':62,'D#4':63,'E4':64,'F4':65,'F#4':66,'G4':67,'G#4':68,'A4':69,'A#4':70,'B4':71,
   
   'C5':72,'C#5':73,'D5':74,'D#5':75,'E5':76,'F5':77,'F#5':78,'G5':79,'G#5':80,'A5':81,'A#5':82,'B5':83,
   
   'C6':84,'C#6':85,'D6':86,'D#6':87,'E6':88,'F6':89,'F#6':90,'G6':91,'G#6':92,'A6':93,'A#6':94,'B6':95,
   
   'C7':96,'C#7':97,'D7':98,'D#7':99,'E7':100,'F7':101,'F#7':102,'G7':103,'G#7':104,'A7':105,'A#7':106,'B7':107,
   
   'C8':108,
   }
# set the output device --------------------------------------------------------
player = pygame.midi.Output(device)  # 定义了一个输出音轨

# set the instrument -----------------------------------------------------------
player.set_instrument(instrument)  # 设置乐器音色

key_value = ('a','s','d','f','g','h','j','q','w','e','r','t','y','u','z','x','c','v','b','n','m','1','2','3','4','5','6','7','i','o','p','k','l',)
key_tone = {
   'a':"C2",'s':"D2",'d':"E2",'f':"F2",'g':"G2",'h':"A2",'j':"B2",
   
   'q':"C3",'w':"D3",'e':"E3",'r':"F3",'t':"G3",'y':"A3",'u':"B3",
   
   'z':"C1",'x':"D1",'c':"E1",'v':"F1",'b':"G1",'n':"A1",'m':"B1",
   
   '1':"C4",'2':"D4",'3':"E4",'4':"F4",'5':"G4",'6':"A4",'7':"B4",
   
   'i':'C5','o':'D5','p':'E5','k':'F5','l':'G5',
   }

while True:
   for event in pygame.event.get(): # 检测事件
       
       if event.type == pygame.QUIT:
           exit()    
       if event.type == pygame.KEYDOWN:
           for i in range(33):
              if event.key == pygame.__dict__[ 'K_'+key_value[i] ]:
                  print('正在发第'+str(i)+'个的音')
                  t1 = time.time()
                  player.note_on(Tone[ key_tone[ key_value[i] ] ], volume)
                 
       elif event.type == pygame.KEYUP:# 按键=松开的话,关闭对应的音调
           for i in range(33):
               if event.key == pygame.__dict__[ 'K_'+key_value[i] ]:
                   print('停止发第'+str(i)+'个')
                   t2 = time.time()
                   t3 = t2 - t1
                   print(str(t3)+'s')
                   player.note_off(Tone[ key_tone[ key_value[i] ] ], volume)

# 循环获取事件,监听事件状态
   for event in pygame.event.get():
       # 判断用户是否点了"X"关闭按钮,并执行if代码段
       if event.type == pygame.QUIT:
           #卸载所有模块
           print("退出")
           pygame.quit()
           #终止程序,确保退出程序
           sys.exit()
   pygame.display.flip() #更新屏幕内容

pygame发声

④窗口显示按键样式和音符:

import pygame.midi
import pygame
import time
import sys
# 初始化设置
volume = 127 # 音量 0-127
pygame.init() # 初始化PYgame

windowSurface=pygame.display.set_mode((800,600)) #建立窗口
# screen = pygame.display.set_mode((400,400))

# 设置窗口标题,即游戏名称
pygame.display.set_caption('键盘钢琴')

#引入字体
f = pygame.font.Font('C:/Windows/Fonts/simhei.ttf',75)
#生成文本信息,第一个参数文本内容;第二个参数,字体是否平滑;
#第三个参数,RGB模式的字体颜色;第四个参数,RGB模式字体背景颜色;
text = f.render("Lebron",True,'deeppink','purple')
# 获得显示对象的rect区域坐标
textRect = text.get_rect()
# 设置显示对象居中
textRect.center = (400,40)
# 将准备好的文本信息,绘制到主屏幕 Screen 上。
windowSurface.blit(text,textRect)

device = 0     # device number in win10 laptop
instrument = 0 #乐器 http://www.ccarh.org/courses/253/handout/gminstruments/
# initize Pygame MIDI ----------------------------------------------------------
pygame.midi.init()# PYGAMEMIDI库的初始化

Tone = {  # 音调字典
   'A0':21,'A#0':22,'B0':23,
   'C1':24,'C#1':25,'D1':26,'D#1':27,'E1':28,'F1':29,'F#1':30,'G1':31,'G#1':32,'A1':33,'A#1':34,'B1':35,
   'C2':36,'C#2':37,'D2':38,'D#2':39,'E2':40,'F2':41,'F#2':42,'G2':43,'G#2':44,'A2':45,'A#2':46,'B2':47,
   'C3':48,'C#3':49,'D3':50,'D#3':51,'E3':52,'F3':53,'F#3':54,'G3':55,'G#3':56,'A3':57,'A#3':58,'B3':59,
   'C4':60,'C#4':61,'D4':62,'D#4':63,'E4':64,'F4':65,'F#4':66,'G4':67,'G#4':68,'A4':69,'A#4':70,'B4':71,
   'C5':72,'C#5':73,'D5':74,'D#5':75,'E5':76,'F5':77,'F#5':78,'G5':79,'G#5':80,'A5':81,'A#5':82,'B5':83,
   'C6':84,'C#6':85,'D6':86,'D#6':87,'E6':88,'F6':89,'F#6':90,'G6':91,'G#6':92,'A6':93,'A#6':94,'B6':95,
   'C7':96,'C#7':97,'D7':98,'D#7':99,'E7':100,'F7':101,'F#7':102,'G7':103,'G#7':104,'A7':105,'A#7':106,'B7':107,
   'C8':108,
   }
# set the output device --------------------------------------------------------
player = pygame.midi.Output(device)#定义了一个输出音轨

# set the instrument -----------------------------------------------------------
player.set_instrument(instrument)#设置乐器音色


key_tone = {
    '1':"C5", '2':"D5", '3':"E5", '4':"F5", '5':"G5", '6':"A5", '7':"B5",
    'q':"C3", 'w':"D3", 'e':"E3", 'r':"F3", 't':"G3", 'y':"A3", 'u':"B3",
    'a':"C4", 's':"D4", 'd':"E4", 'f':"F4", 'g':"G4", 'h':"A4", 'j':"B4",
    'z':"C2", 'x':"D2", 'c':"E2", 'v':"F2", 'b':"G2", 'n':"A2", 'm':"B2",
   }

text_col = 'black'         # 文本颜色
bd_col1 = 'white'        # 背景颜色(初始值)
bd_col2 = 'red'       # 背景颜色(按下反显值)

def key_color (text,x,y,color): #按键改变颜色(文本内容,坐标x,坐标y,颜色R)
   key = f.render(text,True,text_col,color)
   key_rect = key.get_rect()
   key_rect.center = (x,y)
   windowSurface.blit(key,key_rect)

for key in key_tone.keys(): #在窗口中循环打印字典中的字符key内容
    n=list(key_tone.keys()).index(key) #检索字符 是否在字符串列表中,返回字符所在位置
    key_color(key+' ',50+(n//7)*60+(n%7)*90,150+(n//7)*100,bd_col1)
  
# print( list(key_tone.keys()) )
# print( list(key_tone.keys())[0] )
# print(list(key_tone.keys()).index('r'))

while True:
   for event in pygame.event.get():  # 检测事件
       if event.type == pygame.QUIT:
            #卸载所有模块
            print("退出")
            pygame.quit()
            #终止程序,确保退出程序
            sys.exit()  
       if event.type == pygame.KEYDOWN:
          if chr(event.key) in key_tone.keys():#所按下的按键,在音符键盘的字典中
              n=list(key_tone.keys()).index(chr(event.key))#检索字符 是否在字符串列表中,返回字符所在位置
              key_color(chr(event.key)+' ',50+(n//7)*60+(n%7)*90,150+(n//7)*100,bd_col2)
              
              print('正在发'+key_tone[chr(event.key)]+'音')
              t1 = time.time() #记录t1的时间戳
              player.note_on(Tone[key_tone[chr(event.key)]], volume)
       elif event.type == pygame.KEYUP:# 按键=松开的话,关闭对应的音调
           if chr(event.key) in key_tone.keys():#所按下的按键,在音符键盘的字典中
               
               n=list(key_tone.keys()).index(chr(event.key))#检索字符 是否在字符串列表中,返回字符所在位置
               key_color(chr(event.key)+' ',50+(n//7)*60+(n%7)*90,150+(n//7)*100,bd_col1)
               
               print('停止发'+key_tone[chr(event.key)]+'音')
               t2 = time.time() #记录t2的时间戳
               t3 = t2 - t1    #根据t1和t2的时间戳,计算t3时间差
               print(str(t3)+'s') #打印出时间差
               player.note_off(Tone[key_tone[chr(event.key)]], volume)
       pygame.display.flip() #更新
       time.sleep(0.005) #每0.005s循环一次

窗口显示按键样式和音符

 ⑤udp通讯:

# -*- coding: utf-8 -*-
import socket
import time

#client 发送端
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
PORT = 8008

while True:
      start = time.time()  #获取当前时间
      print(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(start)))  #以指定格式显示当前时间
      msg=input("本客户端192.168.43.131,请输入要发送的内容:")  
      server_address = ("192.168.43.82", PORT)  # 接收方 服务器的ip地址和端口号
      client_socket.sendto(bytes(msg.encode("utf-8")), server_address) #将msg内容发送给指定接收方
      now = time.time() #获取当前时间
      run_time = now-start #计算时间差,即运行时间
      print(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(now)))
      print("run_time: %d seconds\n" %run_time)
      time.sleep(1)
###

以上是客户端,以下是服务端

# -*- coding: utf-8 -*-
import pygame.midi
import socket  #导入socket模块
import time #导入time模块

      #server 接收端
      # 设置服务器默认端口号
PORT = 8008
      # 创建一个套接字socket对象,用于进行通讯
      # socket.AF_INET 指明使用INET地址集,进行网间通讯
      # socket.SOCK_DGRAM 指明使用数据协议,即使用传输层的udp协议
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
address = ("", PORT)
server_socket.bind(address)  #为服务器绑定一个固定的地址,ip和端口
server_socket.settimeout(10)  #设置一个时间提示,如果10秒钟没接到数据进行提示

device = 0     # device number in win10 laptop
instrument = 0 #乐器 http://www.ccarh.org/courses/253/handout/gminstruments/
# initize Pygame MIDI ----------------------------------------------------------
pygame.midi.init()# PYGAMEMIDI库的初始化
# set the output device --------------------------------------------------------
player = pygame.midi.Output(device)#定义了一个输出音轨
volume = 127 # 音量 0-127
# set the instrument -----------------------------------------------------------
player.set_instrument(instrument)#设置乐器音色
Tone = {  # 音调字典
   'A0':21,'AS0':22,'B0':23,
   'C1':24,'CS1':25,'D1':26,'DS1':27,'E1':28,'F1':29,'FS1':30,'G1':31,'GS1':32,'A1':33,'AS1':34,'B1':35,
   'C2':36,'CS2':37,'D2':38,'DS2':39,'E2':40,'F2':41,'FS2':42,'G2':43,'GS2':44,'A2':45,'AS2':46,'B2':47,
   'C3':48,'CS3':49,'D3':50,'DS3':51,'E3':52,'F3':53,'FS3':54,'G3':55,'GS3':56,'A3':57,'AS3':58,'B3':59,
   'C4':60,'CS4':61,'D4':62,'DS4':63,'E4':64,'F4':65,'FS4':66,'G4':67,'GS4':68,'A4':69,'AS4':70,'B4':71,
   'C5':72,'CS5':73,'D5':74,'DS5':75,'E5':76,'F5':77,'FS5':78,'G5':79,'GS5':80,'A5':81,'AS5':82,'B5':83,
   'C6':84,'CS6':85,'D6':86,'DS6':87,'E6':88,'F6':89,'FS6':90,'G6':91,'GS6':92,'A6':93,'AS6':94,'B6':95,
   'C7':96,'CS7':97,'D7':98,'DS7':99,'E7':100,'F7':101,'FS7':102,'G7':103,'GS7':104,'A7':105,'AS7':106,'B7':107,
   'C8':108,
   }

player.note_on(Tone['C3'], volume)
time.sleep(1)
player.note_off(Tone['C3'], volume)


while True:
    #正常情况下接收数据并且显示,如果10秒钟没有接收数据进行提示(打印 "time out")
    #当然可以不要这个提示,那样的话把"try:" 以及 "except"后的语句删掉就可以了
  try:  
    now = time.time()  #获取当前时间
                        # 接收客户端传来的数据 recvfrom接收客户端的数据,默认是阻塞的,直到有客户端传来数据
                        # recvfrom 参数的意义,表示最大能接收多少数据,单位是字节
                        # recvfrom返回值说明
                        # receive_data表示接受到的传来的数据,是bytes类型
                        # client  表示传来数据的客户端的身份信息,客户端的ip和端口,元组
    receive_data, client = server_socket.recvfrom(1024)
    tone_temp = str(receive_data,'utf-8')#bytes转换为str字符串
    if tone_temp in Tone.keys():
        player.note_on(Tone[tone_temp], volume)
      
    print(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(now))) #以指定格式显示时间
    print("来自客户端%s,发送的%s\n" % (client, receive_data))  #打印接收的内容
  except socket.timeout:  #如果10秒钟没有接收数据进行提示(打印 "time out")
    print("time out")
 

⑥wifi电子键盘 每个ESP32触摸键盘能够使服务器演奏对应钢琴音:

服务器用pycharm  发送端用thonny

#用thonny 做客户端 pycharm 做服务端
#用thonny 做客户端 pycharm 做服务端
def is_legal_wifi(essid, password):
   '''
   判断WIFI密码是否合法
   '''
   if len(essid) == 0 or len(password) == 0:
       return False
   return True

def do_connect():
   import json
   import network
   
   # 尝试读取配置文件wifi_confi.json,这里我们以json的方式来存储WIFI配置
   # wifi_config.json在根目录下
   
   # 若不是初次运行,则将文件中的内容读取并加载到字典变量 config
   try:
       with open('wifi_config.json','r') as f:
           config = json.loads(f.read())
   # 若初次运行,则将进入excpet,执行配置文件的创建        
   except:
       essid = ''
       password = ''

       while True:
           essid = input('wifi name:') # 输入essid
           password = input('wifi passwrod:') # 输入password

           if is_legal_wifi(essid, password):
               config = dict(essid=essid, password=password) # 创建字典
               with open('wifi_config.json','w') as f:
                   f.write(json.dumps(config)) # 将字典序列化为json字符串,存入wifi_config.json
               break
           else:
               print('ERROR, Please Input Right WIFI')
   
   #以下为正常的WIFI连接流程        
   wifi = network.WLAN(network.STA_IF)  
   if not wifi.isconnected():
       print('connecting to network...')
       wifi.active(True)
       wifi.connect(config['essid'], config['password'])
       import utime

       for i in range(200):
           print('第{}次尝试连接WIFI热点'.format(i))
           if wifi.isconnected():
               break
           utime.sleep_ms(100) #一般睡个5-10秒,应该绰绰有余
       
       if not wifi.isconnected():
           wifi.active(False) #关掉连接,免得repl死循环输出
           print('wifi connection error, please reconnect')
           import os
           # 连续输错essid和password会导致wifi_config.json不存在
           try:
               os.remove('wifi_config.json') # 删除配置文件
           except:
               pass
           do_connect() # 重新连接
       else:
           print('network config:', wifi.ifconfig())




import socket
import time
from machine import Pin
from time import sleep
LED=Pin(2,Pin.OUT)

do_connect()
LED.on()

#多键触摸发声
#-----------------------------------
from machine import TouchPad, Pin,  #引用touch库,GPIO库,PWM库
from time import sleep #引用time库
touch_do=TouchPad(Pin(13)) #创建 DO音 TouchPad对象
touch_re=TouchPad(Pin(12)) #创建 RE音 TouchPad对象
touch_mi=TouchPad(Pin(14)) #创建 MI音 TouchPad对象
touch_fa=TouchPad(Pin(27)) #创建 FA音 TouchPad对象
touch_so=TouchPad(Pin(33)) #创建 SO音 TouchPad对象
touch_la=TouchPad(Pin(32)) #创建 LA音 TouchPad对象
touch_si=TouchPad(Pin(15)) #创建 SI音 TouchPad对象
touch_si=TouchPad(Pin(4)) #创建 SI音 TouchPad对象




# LED点亮说明WIFI连接正常

#client 发送端
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = ("192.168.43.82", 8008)  # 接收方 服务器的ip地址和端口号
while True:
   #循环体
#     A=touch_do.read()
#     B=touch_re.read()
#     C=touch_mi.read()
#     D=touch_fa.read()
#     E=touch_so.read()
#     F=touch_la.read()
#     G=touch_si.read()
#     H=touch_si.read()
   
   PINn = (13,12,14,27,33,32,15,4)#元组
   TPx = [TouchPad(Pin(i)) for i in PINn]
   Tone2 = (
   "A3",
   "A7",
   "A5",
   "B3",
   "D3",
   "C7",
   "B5",
   "A1",
   
   )
 
   for ton,tp in zip(Tone2,TPx):
       if tp.read()<200:
           print("已经发送啦!")
           msg = ton
           client_socket.sendto(bytes(msg.encode('utf-8')), server_address) #将msg内容发送给指定接收方
           time.sleep(0.01)

⑦esp32的WiFi连接:

后期这个直接烧录进esp32

import network
import time
#WIFI连接流程
def wifi_connect():
    wifi = network.WLAN(network.STA_IF)  
    wifi.active(True) 
    wifi.connect('', '')

    for i in range(200):
        print('第{}次尝试连接WIFI热点'.format(i))
        if wifi.isconnected():
            break
        time.sleep(0.1) #一般睡个5-10秒,应该绰绰有余
        
    if not wifi.isconnected():
        wifi.active(False) #关掉连接,免得repl死循环输出
        print('wifi connection error, please reconnect')
    else:
        print('network config:', wifi.ifconfig()) 

前端:

①电子琴的完整数据结构  和点击发声:-

<template>
    <div class="frame">
        <div>
            <!-- 操作区域(退格、清空、播放) -->
        </div>
        <div class="voice">
            <!-- 这里绘制简谱UI -->
        </div>

        <div class="piano">
            <div class="piano_group" v-for="i in 5" :key="i">
                <!-- 这里绘制钢琴UI -->
                <div class="white_keys">
                    <div class="white-key" v-for="key in piano_data[i-1].white_keys" :key="key"
                        @mousedown="mouseDown(key)" @mouseup="mouseup(key)">
                        {{key.note}}
                    </div>
                </div>


                <div class="black_keys">
                    <template v-for="(key,index) in piano_data[i-1].black_keys">
                        <div :key="index" v-if="key.note==''" class="black_key black_key_empty"></div>
                        <div :key="index" v-else class="black_key" @mousedown="mouseDown(key)" @mouseup="mouseup(key)"></div>
                    </template>
                    <!-- template是对用户隐藏的HTML容器 重复代码-->

                </div>
            </div>

        </div>
    </div>
</template>

<script>
    export default {
        name: 'Index',
        components: {},
        data() {
            return {
                // websocket相关参数
                ws: null,

                // 基础数据
                //白键对应显示
                baseWhiteNotes: ['c', 'd', 'e', 'f', 'g', 'a', 'b'],
                //黑键对应显示
                baseBlackNotes: ['cs', 'ds', '', 'fs', 'gs', 'as'],
                //音符时长
                duration: [125, 250, 500, 1000, 2000],

                piano_data: [],
                //时间差
                diff_time: 0,

                //黑键数据结构
                //白键数据结构
                //音符键盘对应字典
                //乐谱
            }
        },
        methods: {
            // 退格
            // 清空乐谱
            // 播放乐谱
            // 按下琴键
            mouseDown(key) {
                let now = new Date()
                this.diff_time = now.getTime()

                console.log("我按下了")
                console.log(key)

                let audio = document.createElement("audio")
                audio.src = require("../assets/sound/" + key.note + ".mp3")
                audio.play()

            },
            // 松开琴键
            mouseup(key) {
                let now = new Date()
                this.diff_time = now.getTime() - this.diff_time
                console.log(this.diff_time)

                console.log("我松开了")
                console.log(key)

                let diff = 8888
                let diff_index = -1

                this.duration.forEach((element, index) => {
                    if (Math.abs(this.diff_time - element) < diff) {
                        diff = Math.abs(this.diff_time - element)
                        diff_index = index
                    }


                });
                console.log("我按了" + this.diff_time)
                console.log("我找到距离我最近的音符是:")
                console.log(this.duration[diff_index])

            }
        },
        created() {

            for (let i = 0; i < 5; i++){
                let group = {

                    black_keys: [],
                    white_keys: []

                }


            let white_list = []
            this.baseWhiteNotes.forEach((element, index) => {
                let key = {
                    note: element + (i+2),
                    notenum: index + 1,
                    lh: i+2,
                    half: false

                }
                white_list.push(key)
            });
            group.white_keys = white_list
            this.piano_data.push(group)


            // baseWhiteNotes数字对象(array)  element一个对象 完整的forEach
            let black_List = []
            this.baseBlackNotes.forEach((element, index) => {
                let key = {}
                key = {
                    note: index == 2 ? "" : element + (i+2),
                    notenum: index + 1,
                    lh: i+2,
                    half: true
                }
                black_List.push(key)
            });
            group.black_keys = black_List
            this.piano_data.push(group)

            //构建数据结构
            //建立websocket连接
        }
        },

        beforeDestroy() {
            //销毁连接
        },

        mounted() {}
    }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
    .frame {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: flex-start;
        background-color: rgb(53, 29, 22);
        height: 100vh;
    }

    .voice {
        border: 1px solid #eee;
        box-shadow: 5px 5px 5px #666;
        width: 620px;
        /* height: 520px; */
        overflow-y: auto;
        padding: 190px;
        background-color: wheat;
    }

    .piano {
        display: flex;
        align-items: center;
        justify-content: center;
        margin-top: 40px;
    }

    .white_keys {
        display: flex;
        flex-direction: row;
    }

    .white-key {
        width: 40px;
        height: 300px;
        border: 1px solid #999;
        /* 边框  border——width宽度 border——style样式 border——color颜色 */
        margin-left: -1px;
        /* 左边距 */
        box-shadow: 2px 2px 2px #666;
        /* 盒子阴影 */
        display: flex;
        align-items: flex-end;
        justify-content: center;
        background-color: #FFF;
    }

    .white-key:active {
        background-color: #666;
    }

    .piano_group {
        position: relative;
    }


    .black_keys {
        position: absolute;
        /* 关于css中的position ,static默认位置,relative相对定位,absolute,fixed固定位置 */
        width: calc(100% - 60px);
        height: 200px;
        top: 0;
        left: 25px;
        display: flex;
        flex-direction: row;
        align-items: flex-start;
        justify-content: space-between;
    }

    .black_key {
        width: 30px;
        height: 200px;
        background-color: #000;
        z-index: 99;
        box-shadow: 2px 2px 2px 1px #999;
        border-block-start: 5px;
        border-block-end: 5px;
    }
    .black_key:active {
        background-color: red;
    }

    .black_key_empty {
        z-index: -1;
        /* 属性指定一个元素的堆叠顺序,为正数会在其上,为负数则在其下 */
    }
</style>

②建立服务端和前端的websocket连接:

服务端:

from flask import Flask  #Flask服务器库
from flask_sockets import Sockets  #WS连接库
import pygame.midi #弹奏音乐的库
import pygame      #pygame的库
import json
from concurrent.futures import ThreadPoolExecutor
import socket


instruments = [1,46,25,56]

# 创建线程池执行器
executor = ThreadPoolExecutor(2)
# 初始化pygame设置
volume = 127  # 音量 0-127
device = 0  # device number in win10 laptop
instrument = 1  # 乐器 http://www.ccarh.org/courses/253/handout/gminstruments/
# initize Pygame MIDI ----------------------------------------------------------
pygame.midi.init()  # PYGAMEMIDI库的初始化
# set the output device --------------------------------------------------------
player = pygame.midi.Output(device)  # 定义了一个输出音轨
# set the instrument -----------------------------------------------------------
player.set_instrument(instrument)  # 设置乐器音色
# 初始化设置结束

Tone = {#音调字典
    'C0':12,'CS0':13,'D0':14,'DS0':15,'E0':16,'F0':17,'FS0':18,'G0':19,'GS0':20,'A0':21,'AS0':22,'B0':23, 
    'C1':24,'CS1':25,'D1':26,'DS1':27,'E1':28,'F1':29,'FS1':30,'G1':31,'GS1':32,'A1':33,'AS1':34,'B1':35,  
    'C2':36,'CS2':37,'D2':38,'DS2':39,'E2':40,'F2':41,'FS2':42,'G2':43,'GS2':44,'A2':45,'AS2':46,'B2':47,   
    'C3':48,'CS3':49,'D3':50,'DS3':51,'E3':52,'F3':53,'FS3':54,'G3':55,'GS3':56,'A3':57,'AS3':58,'B3':59,    
    'C4':60,'CS4':61,'D4':62,'DS4':63,'E4':64,'F4':65,'FS4':66,'G4':67,'GS4':68,'A4':69,'AS4':70,'B4':71,  
    'C5':72,'CS5':72,'D5':74,'DS5':75,'E5':76,'F5':77,'FS5':78,'G5':79,'GS5':80,'A5':81,'AS5':82,'B5':83,  
    'C6':84,'CS6':85,'D6':86,'DS6':87,'E6':88,'F6':89,'FS6':90,'G6':91,'GS6':92,'A6':93,'AS6':94,'B6':95,  
    'C7':96,'CS7':97,'D7':98,'DS7':99,'E7':100,'F7':101,'FS7':102,'G7':103,'GS7':104,'A7':105,'AS7':106,'B7':107,
    }

wsSev = {}
app = Flask(__name__)
sockets = Sockets(app)

cheatMode = 0
cheatIndex = 0
currentGroup = ["C4","D4","E4","F4","G4","A4","B4"]

# 服务器启动时,开启ws服务器
def main_app():
    from gevent import pywsgi
    from geventwebsocket.handler import WebSocketHandler
    server = pywsgi.WSGIServer(('192.168.10.106', 9302), app, handler_class=WebSocketHandler)
#     开启udp监听
    executor.submit(udp_conn)
    print("websocket服务启动")
    server.serve_forever()
    
def udp_conn():
    global wsSev
    global cheatMode
    global cheatIndex
    global currentGroup
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    address=('192.168.10.106', 9302)
    udp_socket.bind(address)
    print('UDP监听开启')
    try:
        while True:
            revc_data = udp_socket.recvfrom(1024)
            print(str(revc_data[0], encoding = "utf-8"))
            noteinfo = json.loads(str(revc_data[0], encoding = "utf-8"))
            print(cheatMode)
            if cheatMode == 1 and str(noteinfo["status"])=='1':
                noteinfo["note"] = currentGroup[cheatIndex]
                print(currentGroup[cheatIndex])
                cheatIndex = cheatIndex + 1
                if len(currentGroup) == cheatIndex:
                    cheatIndex = 0
                    
            play(noteinfo)
            if wsSev:
                wsSev.send(json.dumps(noteinfo))
    except:
        wsTarget.close()

# 持续监听客户端发送的数据
def ws_listener():
    global wsSev
    try:
        while True:
            message = wsSev.receive()
            print(message)
            if message is not None:
                noteinfo = json.loads(message)
                play(noteinfo)
                #wsSev.send("我接收到了!")
    except:
        wsSev.close()
    
# 客户端与服务器建立连接的接口
@sockets.route('/connectServer')
def connect_server(socket):
    global wsSev
    print('connected')
    wsSev = socket
    print('接收到客户端的连接')
    #wsSev.send("你已成功连接至服务器")
    ws_listener()

def play(noteinfo):  #使用Pygame调用声卡发声
    global cheatMode
    global cheatIndex
    if str(noteinfo['status']) == "1":
        print("发声")
        print(noteinfo['note'])
        if str(noteinfo['note']) == 'X1':
            cheatMode = 1
            cheatIndex = 0
            print("开启cheat模式")
        elif str(noteinfo['note']) == 'X2':
            cheatMode = 0
            cheatIndex = 0
            print("关闭cheat模式")
        else:
            player.note_on(Tone[noteinfo['note']], volume)  #弹出声音
    else:
        print("停止")
        player.note_off(Tone[noteinfo['note']], volume) #关闭声音
    
if __name__ == "__main__":
    main_app()
 

前端:

1、访问初始化接口,建立连接

this.ws = new WebSocket("ws://9.7.0.65:9303/connectServer");

ws需要作为一个全局对象,在data中初始化

2、持续监听(接收数据)

this.ws.onmessage = ((event) => {
    // 将接收到的数据序列化为JSON结构(Object对象)
    let socketMessage = event.data
    console.log(socketMessage)
});

3、发送数据

this.ws.send('要发送的内容')

icon图标(前端):

用到UI控件      Element:Element - The world's most popular Vue UI framework

注意注意:

        开始前记得,安装和在main.js中引用!

<el-button type="info" circle>播放</el-button>  这样之后,但并不是我想要的结果,接下来有两种方法,第一在button按钮菜单下的图标按钮的下拉代码下选择,第二是icon图标里找寻需要的图标。  图标标签是<i></i>   最终:<el-button type="info" circle><i class="el-icon-caret-right" style="font-size:30px" @click="playSong"></i></el-button>

本项目需要三个icon,第一个:开始播放,第二个:删除前一个音符,第三个删除全部音符

代码如下:

        

            <el-button type="info" circle><i class="el-icon-caret-right" style="font-size:30px" @click="playSong"></i></el-button>

            <el-button type="warning" circle><i class="el-icon-back" style="font-size:30px" @click="playSong"></i></el-button>

            <el-button type="success" circle><i class="el-icon-close" style="font-size:30px" @click="playSong"></i></el-button>

拓展部分:

①作弊模式:

需要三端联调

硬件端代码如下(只有作弊的代码,不全):

from machine import Pin
import time
import socket
import json

p0 = Pin(0, Pin.IN)     # create input pin on GPIO0
print(p0.value())       # get value, 0 or 1

musickeydict = {
    "status":"1",
    "note":"C3"
}
#client 发送端
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = ("9.7.0.65", 9302)  # 接收方 服务器的ip地址和端口号


def sendtone(note,status):
    musickeydict["note"] = note
    musickeydict["status"] = status
    client_socket.sendto(bytes(json.dumps(musickeydict).encode('utf-8')), server_address) #将msg内容发送给指定接收方


key_time = 0 
while True :
    time.sleep(0.01)
    if (p0.value()==0):
        key_time +=1
        if key_time ==3:
            print("cheat")
            sendtone('X1','1')
    else:
        key_time = 0
        

服务器:

运用两块esp32,

假设  a块和b块,

a的esp32的boot键摁下是发送'X1','1'   ,

b的esp32键摁下发送'X2','1'   。

创建全局变量(下)

# 0:正常模式;1:Cheat模式
cheatMode = 0
# 开启Cheat模式时,对应到乐谱的第几个音
cheatIndex = 0

创建乐曲对象 & 导入乐谱(下)

# 创建乐曲对象,以note数组的形式导入
song = ["C4","B3","C4","G4","A4","A4","G4","D4","D4","F4","E4","C4","B3","C4","E4","F4","F4","G4","D4","C4","C4","C4","B3","C4","G4","A4","A4","G4","D4","D4","F4","E4","D4","E4","C4","B3","C4","E4","F4","F4","G4","D4","C4","C4"]

开启/关闭Cheat模式

当接收到X1或X2时,开启/关闭Cheat模式

if str(noteinfo['note']) == 'X1':
    cheatMode = 1
    cheatIndex = 0
    print("开启cheat模式")
elif str(noteinfo['note']) == 'X2':
    cheatMode = 0
    cheatIndex = 0
    print("关闭cheat模式")

当检测到cheat开启:

        

if cheatMode == 1 and str(noteinfo["status"])=='1' and str(noteinfo["note"])!='X1' and str(noteinfo["note"])!='X2':
    noteinfo["note"] = currentGroup[cheatIndex]
    print(currentGroup[cheatIndex])
    cheatIndex = cheatIndex + 1
    if len(currentGroup) == cheatIndex:
        cheatIndex = 0

②画面段简谱显示:

<div class="stave">
    <div v-for="(item,index) in songs" :key="index" class="note">
        <template>
            <div class="key">
                <div v-if="item.lh>0" class="dot"></div>
                <div v-if="item.lh>1" class="dot"></div>
            </div>
            <div class="note-val">
                {{item.notenum}}
                <span v-if="parseFloat(item.notetype)>1">-</span>
                <span v-if="parseFloat(item.notetype)>2">-</span>
                <div class="half-key" v-if="item.note && item.note.indexOf('s')==1">#</div>
            </div>
            <div class="note-type-half" v-if="parseFloat(item.notetype)<=1"></div>
            <div class="note-type-half" v-if="parseFloat(item.notetype)<1"></div>
            <div class="key">
                <div v-if="item.lh<0" class="dot"></div>
                <div v-if="item.lh<-1" class="dot"></div>
            </div>
        </template>
    </div>
</div>
------------------

css:

简谱显示CSS

 .voice {
        margin-top: 30px;
        border: 1px solid #eee;
        box-shadow: 5px 5px 5px #666;
        width: 70vw;
        height: 520px;
        overflow-y: auto;
        padding: 20px;
        background-color: wheat;
    }

    .voice .stave {
        display: flex;
        flex-direction: row;
        align-items: flex-start;
        justify-content: flex-start;
        flex-wrap: wrap;
    }

    .voice .note {
        margin-right: 10px;
        margin-bottom: 10px;
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        justify-content: flex-start;
        height: 80px;
        font-size: 40px;
        min-width: 40px;
    }

    .voice .note-val {
        position: relative;
    }

    .voice .half-key {
        position: absolute;
        font-size: 16px;
        top: 5px;
        left: 25px
    }

    .note-type-half {
        width: 15px;
        height: 1px;
        background-color: #333;
        margin-bottom: 2px;
        margin-left: 2px;
    }

    .voice .key {
        height: 12px;

    }

    .voice .dot {
        width: 5px;
        height: 5px;
        margin: 2px 0;
        border-radius: 50%;
        background-color: #333;
        margin-left: 7px;
    }

    .op-area {
        z-index: 99;
        position: absolute;
        right: 20vw;
        top: 5px;
        display: flex;
        align-items: center;
        justify-content: center;
    }


③切换乐器:

硬件和服务器的联调:

服务器:        

def udp_conn():
    global instruments
    global instrumentIndex
    
    udp_socket = socket.socket(socket.AF_INET , socket.SOCK_DGRAM)
    address=('9.7.5.3',9301)
    udp_socket.bind(address)
    print('UDP监听开启')
    try:
        while True:
            revc_data = udp_socket.recvfrom(1024)
            print(str(revc_data[0], encoding = "utf-8"))
            noteinfo = json.loads(str(revc_data[0], encoding = "utf-8"))
            if str(noteinfo["note"]) == "S1":
                instrumentIndex = instrumentIndex + 1
                if instrumentIndex == 4:
                    instrumentIndex = 1
                print(instruments[instrumentIndex])
                player.set_instrument(instruments[instrumentIndex])

            
                
            elif str(noteinfo["note"]) == "S2":
                instrumentIndex = instrumentIndex - 1
                if instrumentIndex == -1:
                    instrumentIndex = 3
                print(instruments[instrumentIndex])
                player.set_instrument(instruments[instrumentIndex])
            else:
                play(noteinfo)
            if wsSev:
                wsSev.send(json.dumps(noteinfo))
            
    except:
        wsTarget.close()

硬件:

因为每块esp32上只有一个boot键

假设有a和b

a是发送S1的也就是上一个乐器     b是发送S2的也就是上一个乐器

# 先要连上wifi wifi连接代码此处省略……………………      
import socket
import time
import WiFi
import json

WiFi.wifi_connect()


from machine import TouchPad, Pin,PWM  
from time import sleep

print("系统充电")
time.sleep(1)

LED = Pin(2,Pin.OUT)#面包板
# LED = Pin(22,Pin.OUT)#PCB
p0 = Pin(0, Pin.IN, Pin.PULL_UP)     # create input pin on GPIO0
pwm0 = PWM(Pin(23))
freq_val=5
duty_val=0
pwm0.freq(freq_val)
pwm0.duty(duty_val)
# PINn = (32,33,27,13,12,15,14,4)#PCB元组
PINn = (12,13,14,27,33,32,15,4)#面包板元组

TPx = [TouchPad(Pin(i)) for i in PINn]#TPx 所有的触摸管脚定义

musickeydict = {
    "status":"1",
    "note":"G3"
}
# musickeydict["note"] = 'G6'
# musickeydict["status"] = '0'


# print(musickeydict)
def sendtone(note,status):
    musickeydict["note"] = note
    musickeydict["status"] = status
    client_socket.sendto(bytes(json.dumps(musickeydict).encode('utf-8')), server_address) #将msg内容发送给指定接收方

msg = ('D3',#需要发送的信息
       'C3',
       'E3',
       'F3',
       'G3',
       'A3',
       'B3',
       'C4',
       )

# do_connect()# wifi连接
LED.on()# LED点亮说明WIFI连接正常
time.sleep(1)

#client 发送端
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# server_address = ("192.168.43.234", 8008)  # 接收方 服务器的ip地址和端口号
server_address = ("9.7.5.3", 9301)  # 接收方 服务器的ip地址和端口号

key_time = 0 #按键时间
key_time1 = 0 #按键时间
keychar = ''
def dzq():
    '电子琴触摸按键子程序'
    global key_time,keychar,key_time1
    
    for ton,tp in zip(msg,TPx):
        '遍历所有的触摸按钮'
        tp_val = 10000
        try:
            tp_val = tp.read()
        except:
            print("触摸读取错误")
            break
        if tp_val<200:#有触摸按键按下
            key_time += 1#按键时间+1
            if key_time == 3:
                try:
                    sendtone(ton,'1')
#                     musickeydict["note"] = ton
#                     musickeydict["status"] = '1'
#                     client_socket.sendto(bytes(json.dumps(musickeydict).encode('utf-8')), server_address) #将msg内容发送给指定接收方
                except:
                    print("发送失败")
            keychar = ton
            LED.on()
            break
    else:
        '如果运行完所有的循环,没有break,就执行这个else'
        if key_time != 0:
            sendtone(keychar,'0')
#             musickeydict["note"] = keychar
#             musickeydict["status"] = '0'
#             client_socket.sendto(bytes(json.dumps(musickeydict).encode('utf-8')), server_address) #将msg内容发送给指定接收方
        key_time = 0#按键时间清零
        LED.off()
    if (p0.value()==0):
        key_time1 +=1
        if key_time1 ==3:
            print("切换下一个乐器")
            sendtone("S1",1)

另外一块只用将S1变为S2即可     

前端和后端的联调。                                                                                                                                                                                                                                                    前端:

html:

            <el-button type="danger" circle @click="playSong('top')">

                <i class="el-icon-d-arrow-left" style="font-size:45px"

                    ></i>

            </el-button>

            <el-button type="primary" circle  @click="playSong('next')">

                <i class="el-icon-d-arrow-right" style="font-size:45px"

                   ></i>

            </el-button>

定义一个方法如下:

            playSong(info) {

                console.log(info)

                if (info == 'top') {

                        let a ={

                            note:"S1"

                        }

                        this.ws.send(JSON.stringify(a))

                }

                    else {

                        let b ={

                            note:"S2"

                        }

                        this.ws.send(JSON.stringify(b))

                }

后端的代码基本上和udp连接代码一样    (此处省略....)   

增加退格、清空按钮

<el-button type="primary" :disabled="songs.length <=0" circle @click="songs.pop()">
    <i class="el-icon-back" style="font-size:30px"></i>
</el-button>

<el-button type="danger" circle @click="songs=[]">
    <i class="el-icon-delete" style="font-size:30px"></i>
</el-button>

通过ElementUI,新增歌单选择功能

  1. 创建歌曲文件,如 mysong1.js、mysong2.js
export function getSong() {
    // 歌曲内容
    let song = [{
        notenum: 5,
        lh: 0,
        half: false,
        notetype: 0,
    }]
    return song
}
  1. 在index.vue中引用歌曲文件(注意,需在script标签的一开始就引用)
    引用mysong1中的getSong方法,取别名为getSong1,后面可以通过getSong1()获取歌曲内容
    import {
        getSong as getSong1
    } from './mysong1'
    import {
        getSong as getSong2
    } from './mysong2'
  1. 创建全局变量,在画面初始化时,初始化歌单
//当前歌单index
songListIndex: 0,
//歌单
songList: [],

创建歌单对象列表,对象中包含3个属性,分别是
speed:n 播放速度(以0.5一拍,n倍速)
song: 歌曲内容
name: 歌曲名称

mounted() {
    this.songList = [{
        speed: 1,
        song: getSong1(
        name: 'Way Back Home'
    }, {
        speed: 2,
        song: getSong2(
        name: '青石巷'
    }, {
        speed: 2,
        song: getSong3(
        name: '菊次郎的夏天'
    }, {
        speed: 1,
        song: getSong4(
        name: '起风了'
    }, {
        speed: 1.5,
        song: getSong5(
        name: '白月光与朱砂痣'
    }]
  1. DOM端
    v-model 绑定一个变量,为当前选中的数组的value
    @change 变更时调用的事件
    el-option 选项
    :name 显示的值
    :value 绑定给v-model的值
<el-select v-model="songListIndex" @change="changeSong" style="margin-right:20px">
    <el-option v-for="(item,index) in songList" :key="index" :label="item.name" :value="index">
    </el-option>
</el-select>
  1. 变更事件
changeSong() {
    // 指向当前对应的歌曲
    this.songs = this.songList[this.songListIndex].song
}
  1. 播放时的速度调整
let duration = this.durationList[key.notetype]
if (element.speed) {
    duration = duration * element.speed
}

硬件部分:

①esp32在面包板上的连接:

②画pcb电路图:

我用的是立创eda

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 三维设计

3d模型

代码优化:

客户端最终显示:

<div class="volumes">

                    <div class="item" v-for="(item,index) in instrumentVolumes" :key="index">

                        <el-image style="width:30px; height:30px;margin-bottom:10px" :src="require('./music.png')">

                        </el-image>

                        <div class="volumes-num">{{instrumentVolumes[index].value}}</div>

                        <el-slider v-model="instrumentVolumes[index].value" :max=127 vertical height="45vh"

                            @change="input_words()">

                        </el-slider>

                    </div>

                </div>

<div class="playSongs">

                <el-select v-model="selectedIndex" @change="changeSong" style="margin-right:20px;">

                    <el-option v-for="(item,index) in optionList" :key="index" :value="index" :label="item.name">

                    </el-option>

 </el-select>

                <el-button type="info" @click="playSongs" style="margin-left:-20px;border-radius:0">

                    <i class="el-icon-caret-right" style="margin-left:-5px"></i>

                </el-button>

            </div>

            <div class="pop">

                <el-button :disabled="songs.length <=0" @click="songs.pop()" circle><i class="el-icon-back"

                        style="font-size:20px"></i>

                </el-button>

                <el-button @click="songs=[]" circle><i class="el-icon-delete" style="font-size:20px"></i>

                </el-button>

            </div>

            <div class="voice">

export default {

        name: 'index',

        components: {},

        data() {

            return {

                options_I: [{

                    name: '乐器1',

                    value: '1',

                }, {

                    name: '乐器2',

                    value: '2',

                }, {

                    name: '乐器3',

                    value: '3',

                }, {

                    name: '乐器4',

                    value: '4',

                }, {

                    name: '乐器5',

                    value: '5',

                }],

                value_I: [],

                options_v: [{

                    value: {

                        1: '0'

                    },

                }, {

                    value: {

                        2: '0'

                    },

                }, {

                    value: {

                        3: '0'

                    },

                }, {

                    value: {

                        4: '0'

                    },

                }, {

                    value: {

                        5: '0'

                    },

                }],

                value_I: [],


 

                value1: 0,

                instrumentVolumes: [{

                    value: 85,

                    isMute: false,

                }, {

                    value: 96,

                    isMute: false,

                }, {

                    value: 118,

                    isMute: false,

                }, {

                    value: 102,

                    isMute: false,

                }, {

                    value: 74,

                    isMute: false,

                }, {

                    value: 66,

                    isMute: false,

                }],

                // 输入的文本内容

                input1: "85",

                input2: "96",

                input3: "118",

                input4: "102",

                input5: "74",

methods: {

            formatTooltip(val) {

                let dict = {

                    volume: this.input

                }

                this.ws.send(JSON.stringify(dict))

                return val / 99;

            },

            input_words() {

                let dict = {

                    intr1: this.instrumentVolumes[0].value,

                    intr2: this.instrumentVolumes[1].value,

                    intr3: this.instrumentVolumes[2].value,

                    intr4: this.instrumentVolumes[3].value,

                    intr5: this.instrumentVolumes[4].value,

                    intr5: this.instrumentVolumes[5].value,

                    send: '1'

                    // volume: this.input

                }

                console.log(dict)

                this.ws.send(JSON.stringify(dict))

            },

简谱显示 需要import 比如说song1  song2......

服务器:

import pygame.midi #弹奏音乐的库
import json
import socket
from flask import Flask  #Flask服务器库
from flask_sockets import Sockets  #WS连接库
from concurrent.futures import ThreadPoolExecutor #引用线程池
from gevent import pywsgi
from geventwebsocket.handler import WebSocketHandler

# 创建线程池执行器
executor = ThreadPoolExecutor(3)

# 初始化pygame设置


device = 0  # device number in win10 laptop
# initize Pygame MIDI ----------------------------------------------------------
pygame.midi.init()  # PYGAMEMIDI库的初始化
# set the output device --------------------------------------------------------
player = pygame.midi.Output(device)
# 定义了一个输出音轨
player.set_instrument(1) # 设置乐器音色 乐器 http://www.ccarh.org/courses/253/handout/gminstruments/
# set the instrument ------------------------------------0000000000000000000000000-----------------------
# 初始化设置结束
cheats = 0
index = 0
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

music = ['G3','G3','G3','G3','G3','G3','G3','G3','G3','G3','G3','G3',
         'E4','E4','E4','E4','E4','E4','E4','E4','E4','E4','E4','E4',
         'D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4',
         'E4','E4','E4','E4','E4','E4','E4','E4','E4','E4','E4','E4',
         'E3','E3','E3','E3','E3','E3','E3','E3','E3','E3','E3','E3',
         'C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4',
         'B3','B3','B3','B3','B3','B3','B3','B3','B3','B3','B3','B3',
         'A3','A3','A3','A3','A3','A3','A3','A3','A3','A3','A3','A3',
         'A3','A3','A3','A3','A3','A3','A3','A3','A3','A3','A3','A3',
         'D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4',
         'C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4',
         'D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4',
         'A3','A3','A3','A3','A3','A3','A3','A3','A3','A3','A3','A3',
         'C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4',
         'D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4',
         'D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4',
         'D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4',
         'D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4',
         'C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4',
         'D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4',
         'E4','E4','E4','E4','E4','E4','E4','E4','E4','E4','E4','E4',
         'D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4',
         'E3','E3','E3','E3','E3','E3','E3','E3','E3','E3','E3','E3',
         'E4','E4','E4','E4','E4','E4','E4','E4','E4','E4','E4','E4',
         'D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4',
         'E4','E4','E4','E4','E4','E4','E4','E4','E4','E4','E4','E4',
         'E3','E3','E3','E3','E3','E3','E3','E3','E3','E3','E3','E3',
         'C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4',
         'B3','B3','B3','B3','B3','B3','B3','B3','B3','B3','B3','B3',
         'A3','A3','A3','A3','A3','A3','A3','A3','A3','A3','A3','A3',
         'G3','G3','G3','G3','G3','G3','G3','G3','G3','G3','G3','G3',
         'A3','A3','A3','A3','A3','A3','A3','A3','A3','A3','A3','A3',
         'C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4',
         'C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4',
         'D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4',
         'E4','E4','E4','E4','E4','E4','E4','E4','E4','E4','E4','E4',
         'D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4',
         'D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4','D4',
         'B3','B3','B3','B3','B3','B3','B3','B3','B3','B3','B3','B3',
         'C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4','C4',
         'C4','C4','C4','C4','C4','C4',]

Tone = {#音调字典
    'C0':12,'CS0':13,'D0':14,'DS0':15,'E0':16,'F0':17,'FS0':18,'G0':19,'GS0':20,'A0':21,'AS0':22,'B0':23, 
    'C1':24,'CS1':25,'D1':26,'DS1':27,'E1':28,'F1':29,'FS1':30,'G1':31,'GS1':32,'A1':33,'AS1':34,'B1':35,  
    'C2':36,'CS2':37,'D2':38,'DS2':39,'E2':40,'F2':41,'FS2':42,'G2':43,'GS2':44,'A2':45,'AS2':46,'B2':47,   
    'C3':48,'CS3':49,'D3':50,'DS3':51,'E3':52,'F3':53,'FS3':54,'G3':55,'GS3':56,'A3':57,'AS3':58,'B3':59,    
    'C4':60,'CS4':61,'D4':62,'DS4':63,'E4':64,'F4':65,'FS4':66,'G4':67,'GS4':68,'A4':69,'AS4':70,'B4':71,  
    'C5':72,'CS5':72,'D5':74,'DS5':75,'E5':76,'F5':77,'FS5':78,'G5':79,'GS5':80,'A5':81,'AS5':82,'B5':83,  
    'C6':84,'CS6':85,'D6':86,'DS6':87,'E6':88,'F6':89,'FS6':90,'G6':91,'GS6':92,'A6':93,'AS6':94,'B6':95,  
    'C7':96,'CS7':97,'D7':98,'DS7':99,'E7':100,'F7':101,'FS7':102,'G7':103,'GS7':104,'A7':105,'AS7':106,'B7':107,
    'C8':108}

times = 0

def send(msg,server_address):
    global times
    try:
        times += 1
        print(times)
        #将msg内容发送给指定接收方,要bytes(zmsg.encode('utf-8'))转一下字符格式
        client_socket.sendto(bytes(msg.encode('utf-8')),server_address)
    except:
        print("数据发送失败!")
        
def Play(noteinfo):  #使用Pygame调用声卡发声
    if noteinfo['status'] == '1':
        #print(noteinfo['instrument'])
        player.set_instrument(noteinfo['instrument'])
        player.note_on(Tone[noteinfo['note']],noteinfo['volume'])  #弹出声音 
    else:
        player.set_instrument(noteinfo['instrument'])
        print(noteinfo)
        player.note_off(Tone[noteinfo['note']],noteinfo['volume']) #关闭声音
        

wsSev = {}  #建立一个新的服务器变量
app = Flask(__name__)
sockets = Sockets(app)

# 服务器启动时,开启ws服务器
def main_app():
    # 设定服务器网址和端口
    server = pywsgi.WSGIServer(('192.168.43.82',9300),app,handler_class=WebSocketHandler)
    #开启udp监听
    executor.submit(udp_conn)
    print("websocket服务启动")
    server.serve_forever() #服务器始终处于监听状态
    
 
# udp监听硬件端发送的数据
def udp_conn():
    global wsSev,instrument,noteinfo,cheats,index
    server_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    address=('',9302)    #服务器地址,但是要另外开一个端口
    server_socket.bind(address)
    print('UDP监听开启')
    try:
        while True:
            revc_data,client = server_socket.recvfrom(1024) 
            if str(revc_data,encoding="utf-8") == "on":
                print(bytes.decode(revc_data),client[0])
                noteinfo = {'volume':127}
                send(json.dumps({'computer':'1'}),(client[0],9301))
                print([0])
            else:
                noteinfo = json.loads(str(revc_data,encoding = "utf-8"))
                print(str(revc_data,encoding = "utf-8"))
                if noteinfo['note'] == 'X1':
                    cheats += 1
                elif cheats > 0:
                    noteinfo["note"] = music[index]
                    print('000000000000000000000')
                    print(music[index])
                    Play(noteinfo)
                    index += 1
                    if index == 40 * 6 - 1:
                        index = 0
                if wsSev:
                    wsSev.send(json.dumps(noteinfo))
    except:
        wsTarget.close()

# 持续监听客户端发送的数据
def ws_listener():
    global wsSev
    try:
        while True:
            message = wsSev.receive()  #收到WS数据
            print(message)
            if message is not None:  #如果内容不为空
                noteinfo = json.loads(message)
                if noteinfo['send'] == '1':
                    print(str(json.dumps(noteinfo)))
                    send(json.dumps(noteinfo),("192.168.1.220",9301))
                else:
                    Play(noteinfo)  #弹奏音乐
                #wsSev.send("我接收到了!")  
    except:
        wsSev.close()

# 客户端与服务器建立连接的接口
#当客户端调用接口时,自动进入connect_server(socket)函数,然后就默认连接上了
@sockets.route('/connectServer') 
def connect_server(socket):
    global wsSev
    print('connected')
    wsSev = socket
    print('接收到客户端的连接')
    #wsSev.send("你已成功连接至服务器")  #反馈客户端连接的信息
    ws_listener()


#主程序        
if __name__ == "__main__":
    main_app()

    


硬件:

# -*- coding: utf-8 -*-
import socket
import time
import sys
import json
import network
import _thread
from machine import Pin, PWM, TouchPad


LED = Pin(2, Pin.OUT)  #定义GPIO2的口
p18 = Pin(18, Pin.IN)
PINNum = (12,13,14,27,33,32,15,4)  # 定义触摸功能的引脚号,元组
TP8 = [TouchPad(Pin(i)) for i in PINNum]  # 启用引脚的触摸功能
pwm0 = PWM(Pin(23))  # 定义Pin26脚产生一个PWM脉冲驱动蜂鸣器
key = ('B2','C3','D3','E3','F3','G3','A3','B3')  # 定义不同音调 不加窗口
BuzzerKey = (523, 586, 658, 697, 783, 879, 987, 1045)  # 定义不同音调的频率
freq_val = 0  # 频率的临时量
duty_val = 0  # 占空比的临时量
msg = ''
key_time = 0 #记录键盘按下的次数
connect = 6
mode = 'buzzer'
message = {'intr1':'127','intr2':'127','intr3':'127','intr4':'127','intr5':'127'}
ii = 0


client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = ("192.168.43.82", 9302)  # 接收方 服务器的ip地址和端口号

server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
address = ("", 9301)   
server_socket.bind(address)  # 为服务器绑定一个固定的地址,ip和端口
server_socket.settimeout(1314/520)  #设置一个时间提示,如果1314/520秒钟没接到数据进行提示

#WIFI连接流程        
wifi = network.WLAN(network.STA_IF)  
wifi.active(True) 
wifi.connect('LEBRON', 'woaini7890')

def tone():
    while True:
        if False:
            key = ('C3','D3','E3','F3','G3','A3','B3','switch')
            BuzzerKey = ()
        elif False:
            key = ('C4','D4','E4','F4','G4','A4','B4','switch')
            BuzzerKey = ()
        elif False:
            key = ('C5','D5','E5','F5','G5','A5','B5','switch')
            BuzzerKey = ()
        elif False:
            key = ('C6','D6','E6','F6','G6','A6','B6','switch')
            BuzzerKey = ()
        

def send(msg,server_address):
    try:
        print(msg)
        #将msg内容发送给指定接收方,要bytes(msg.encode('utf-8'))转一下字符格式
        client_socket.sendto(bytes(msg.encode('utf-8')), server_address)                    
    except:
        print("数据发送失败!")
        
def computer_Play():
    global key_time,mode,pwm0,ii
    msg = {'note':'X1','status':'1','volume':int(message['intr1']),'instrument':0,'piano':'1'}
    pwm0.duty(0)
    LED.off()
    while True:
        if mode == 'buzzer':
            break
        for i, tp in enumerate(TP8):  # 遍历所有的触摸按键,前面加序号,从0开始
            try:
                read = tp.read()
            except:
                read = 200
            if read < 200:
                LED.on()
                key_time += 1 #按键次数加1
                if key_time == 2:
                    msg = {'note':key[i],'status':'1','volume':int(message['intr1']),'instrument':0,'piano':'1'} 
                    send(json.dumps(msg),server_address)
                    msg = {'note':key[i],'status':'1','volume':int(message['intr2']),'instrument':1,'piano':'0'}
                    send(json.dumps(msg),server_address)
                    msg = {'note':key[i],'status':'1','volume':int(message['intr3']),'instrument':8,'piano':'0'}
                    send(json.dumps(msg),server_address)
                    msg = {'note':key[i],'status':'1','volume':int(message['intr4']),'instrument':9,'piano':'0'}
                    send(json.dumps(msg),server_address)
                    msg = {'note':key[i],'status':'1','volume':int(message['intr5']),'instrument':10,'piano':'0'}
                    send(json.dumps(msg),server_address)
                    ii = i
                break
        else:
            if key_time != 0:
                LED.off()
                msg = {'note':key[ii],'status':'0','volume':int(message['intr1']),'instrument':0,'piano':'1'}
                send(json.dumps(msg),server_address)
                msg = {'note':key[ii],'status':'0','volume':int(message['intr2']),'instrument':1,'piano':'0'}
                send(json.dumps(msg),server_address)
                msg = {'note':key[ii],'status':'0','volume':int(message['intr3']),'instrument':8,'piano':'0'}
                send(json.dumps(msg),server_address)
                msg = {'note':key[ii],'status':'0','volume':int(message['intr4']),'instrument':9,'piano':'0'}
                send(json.dumps(msg),server_address)
                msg = {'note':key[ii],'status':'0 ','volume':int(message['intr5']),'instrument':10,'piano':'0'}
                send(json.dumps(msg),server_address)
                time.sleep(0.01)
                key_time = 0
         
def buzzer_Play():  # 电子琴的主程序
    global mode,pwm0
    pwm0.duty(0)
    LED.off()
    while True:
        if mode == 'computer':
            break
        for i,tp in enumerate(TP8):  # 遍历所有的触摸按键,前面加序号,从0开始
            try:
                read = tp.read()
            except:
                read = 200
            if read < 200:
                freq_val = BuzzerKey[i]  # 频率设为按下的音调
                duty_val = 512  # 占空比打开
                LED.on()  # 开灯
                break
        else:
            freq_val = 0 
            duty_val = 0  # 关闭占空比
            LED.off()
        # 上面的程序是根据按钮设置频率和占空比
        if pwm0.freq() != freq_val:  # 如果读到的频率不是现在临时量中的频率,则设置频率和占空比
            pwm0.freq(freq_val)
            time.sleep(0.01)   # 频率和占空比不能同时设置,否则会导致占空比设置失败
            pwm0.duty(duty_val)

def sound(threadName,delay):
    global mode
    while True:
        if mode == 'computer':
            computer_Play()
        elif mode == 'buzzer':
            buzzer_Play()
          
          
_thread.start_new_thread(sound,("newThread",1))
   
   
for i in range(100):
    print('第{}次尝试连接WIFI热点'.format(i))
    if wifi.isconnected():
        break
    time.sleep(0.1) 
    
if not wifi.isconnected():
    wifi.active(False) #关掉连接,免得repl死循环输出
    print('wifi connection error, please reconnect')
    buzzer_Play()
else:
    print('network config:', wifi.ifconfig())
    while True:
    #正常情况下接收数据并且显示,如果1秒钟没有接收数据进行提示(打印 "time out")
    #当然可以不要这个提示,那样的话把"try:" 以及 "except"后的语句删掉就可以了
        try:  
        # 接收客户端传来的数据 recvfrom接收客户端的数据,默认是阻塞的,直到有客户端传来数据
        # recvfrom 参数的意义,表示最大能接收多少数据,单位是字节
        # recvfrom返回值说明
        # receive_data表示接受到的传来的数据,是bytes类型
        # client  表示传来数据的客户端的身份信息,客户端的ip和端口,元组
            receive_data,client = server_socket.recvfrom(1024)
            initial = json.loads(receive_data)
            if initial == {'computer': '1'}:
                pass
            else:
                message = initial
            print(client[0],message)
            connect = 0
            mode = 'computer'
        except:  #如果没有接收数据进行提示(打印 "time out")
            if connect % 3 == 1:
                send('on',server_address)
            elif connect == 12:
                print("time out")
                mode = 'buzzer'
            elif connect == 13:
                connect = 0
            connect += 1   


 

                                

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值