基于ESP8266的鼓采样播放器
基于ESP8266的鼓采样播放器ESP8266ESP8266是相当强大的音频应用与CPU频率160MHz和4MB闪存。本教程的目标是在ESP8266平台上构建合成器,因此我们还将介绍如何添加MIDI输入。PDM DAC还可以用于网络广播和其他音频流应用程序。我们将使用带有Arduino IDE的Wemos D1 Mini板。ESP8266 i2接口ESP8266通过i2s来处理音频。i2s是2个16位串行字、左右通道和一个DMA驱动的移位时钟的高速移位。这个接口通常需要一个外部i2s DAC来将串行流转换为模拟信号。为了使它更容易,我们将基于i2s接口构建一个PDM(脉冲密度调制)DAC。PDM是一种高速率比特流,在44.1KHz的采样率下,它将是其32倍或约1.4MHz。脉冲密度调制是一个1位DAC提供了6dB的动态范围。这将产生大量的噪音,确切地说是90dB。好在噪音的频率范围远高于音频频谱,可以很容易地用低通滤波器过滤掉,只留下音频信号。因此delta-sigma编码我们的16位样本字到PDM将给我们一个16位DAC输出只有一个外部无源滤波器。这是音频输出的原理图:但为什么会连在RX引脚上?那不是串行输入引脚吗?它也是i2s数据输出引脚。让我们展示一些代码:这是第一个测试的设置()。它关闭WiFi收音机,将电源降低到大约15mA,并以44100Hz的采样率为i2s子系统设置引脚和DMA:#include "Arduino.h"
#include "ESP8266WiFi.h"
#include "i2s.h"
#include "i2s_reg.h"
void setup() {
//WiFi.forceSleepBegin();
//delay(1);
system_update_cpu_freq(160);
i2s_begin();
i2s_set_rate(44100);
}下面是输出样本的edac函数:void writeDAC(uint16_t DAC) {
for (uint8_t i=0;i<32;i++) {
i2sACC=i2sACC<<1;
if(DAC >= err) {
i2sACC|=1;
err += 0xFFFF-DAC;
}
else
{
err -= DAC;
}
}
bool flag=i2s_write_sample(i2sACC);
}为了测试DAC,我们生成一个慢正弦波:uint8_t phase;
void loop() {
writeDAC(0x8000+sine);
}正弦波数据:int16_t sine = {
0x0000, 0x0324, 0x0647, 0x096a, 0x0c8b, 0x0fab, 0x12c8, 0x15e2,
0x18f8, 0x1c0b, 0x1f19, 0x2223, 0x2528, 0x2826, 0x2b1f, 0x2e11,
0x30fb, 0x33de, 0x36ba, 0x398c, 0x3c56, 0x3f17, 0x41ce, 0x447a,
0x471c, 0x49b4, 0x4c3f, 0x4ebf, 0x5133, 0x539b, 0x55f5, 0x5842,
0x5a82, 0x5cb4, 0x5ed7, 0x60ec, 0x62f2, 0x64e8, 0x66cf, 0x68a6,
0x6a6d, 0x6c24, 0x6dca, 0x6f5f, 0x70e2, 0x7255, 0x73b5, 0x7504,
0x7641, 0x776c, 0x7884, 0x798a, 0x7a7d, 0x7b5d, 0x7c29, 0x7ce3,
0x7d8a, 0x7e1d, 0x7e9d, 0x7f09, 0x7f62, 0x7fa7, 0x7fd8, 0x7ff6,
0x7fff, 0x7ff6, 0x7fd8, 0x7fa7, 0x7f62, 0x7f09, 0x7e9d, 0x7e1d,
0x7d8a, 0x7ce3, 0x7c29, 0x7b5d, 0x7a7d, 0x798a, 0x7884, 0x776c,
0x7641, 0x7504, 0x73b5, 0x7255, 0x70e2, 0x6f5f, 0x6dca, 0x6c24,
0x6a6d, 0x68a6, 0x66cf, 0x64e8, 0x62f2, 0x60ec, 0x5ed7, 0x5cb4,
0x5a82, 0x5842, 0x55f5, 0x539b, 0x5133, 0x4ebf, 0x4c3f, 0x49b4,
0x471c, 0x447a, 0x41ce, 0x3f17, 0x3c56, 0x398c, 0x36ba, 0x33de,
0x30fb, 0x2e11, 0x2b1f, 0x2826, 0x2528, 0x2223, 0x1f19, 0x1c0b,
0x18f8, 0x15e2, 0x12c8, 0x0fab, 0x0c8b, 0x096a, 0x0647, 0x0324,
0x0000, 0xfcdc, 0xf9b9, 0xf696, 0xf375, 0xf055, 0xed38, 0xea1e,
0xe708, 0xe3f5, 0xe0e7, 0xdddd, 0xdad8, 0xd7da, 0xd4e1, 0xd1ef,
0xcf05, 0xcc22, 0xc946, 0xc674, 0xc3aa, 0xc0e9, 0xbe32, 0xbb86,
0xb8e4, 0xb64c, 0xb3c1, 0xb141, 0xaecd, 0xac65, 0xaa0b, 0xa7be,
0xa57e, 0xa34c, 0xa129, 0x9f14, 0x9d0e, 0x9b18, 0x9931, 0x975a,
0x9593, 0x93dc, 0x9236, 0x90a1, 0x8f1e, 0x8dab, 0x8c4b, 0x8afc,
0x89bf, 0x8894, 0x877c, 0x8676, 0x8583, 0x84a3, 0x83d7, 0x831d,
0x8276, 0x81e3, 0x8163, 0x80f7, 0x809e, 0x8059, 0x8028, 0x800a,
0x8000, 0x800a, 0x8028, 0x8059, 0x809e, 0x80f7, 0x8163, 0x81e3,
0x8276, 0x831d, 0x83d7, 0x84a3, 0x8583, 0x8676, 0x877c, 0x8894,
0x89bf, 0x8afc, 0x8c4b, 0x8dab, 0x8f1e, 0x90a1, 0x9236, 0x93dc,
0x9593, 0x975a, 0x9931, 0x9b18, 0x9d0e, 0x9f14, 0xa129, 0xa34c,
0xa57e, 0xa7be, 0xaa0b, 0xac65, 0xaecd, 0xb141, 0xb3c1, 0xb64c,
0xb8e4, 0xbb86, 0xbe32, 0xc0e9, 0xc3aa, 0xc674, 0xc946, 0xcc22,
0xcf05, 0xd1ef, 0xd4e1, 0xd7da, 0xdad8, 0xdddd, 0xe0e7, 0xe3f5,
0xe708, 0xea1e, 0xed38, 0xf055, 0xf375, 0xf696, 0xf9b9, 0xfcdc
};产生的波形输出为172Hz的正弦波::两件重要的事情:
[*]esp8266是一个实时操作系统,其他的事情都发生在后台。所以不要使用delay()或其他阻塞函数。如果某些事情需要很长时间,请使用yield()。
[*]DMA缓冲区有512个样本长,将在11.5毫秒内耗尽。要获得不间断音频输出,需要在耗尽之前给它输入样本。
请随意尝试并运行。(*有人声称给PDM算法输入一个平坦的0x0001 DAC值将使它在22Hz的嗡嗡声中失败。虽然这是事实,但这是很少发生的极端情况。正常的平线是0x8000,在700KHz时产生50/50的方波,偶尔DAC值下降到1000以下不会持续很长时间,所以在现实世界的波数据中这不是问题。)(* PDM比特率运行在1.4MHz。为了让它以3MHz的更专业的比特率运行,只需将采样率从44.1KHz提高到96KHz。)一个简单的909鼓合成器利用我们的采样知识,我们将做一个简单的909鼓采样播放器。样本播放器是一个11声道全复调44.1KHz 16位1发波播放器。为此,我们需要价值约300Kbyte的44.1KHz鼓样:const uint16_t BD16 PROGMEM = {
40, 85, 137, 144, -30, -347, -609, -785, // 0-7
const uint16_t CP16 PROGMEM = {
-42, 74, -1236, -2741, -3134, -11950, -13578, -7572, // 0-7上面的定义只是16位波数据的一个小样本。我们还需要一些用于示例引擎的声明。uint32_t BD16CNT;
uint32_t CP16CNT;
uint32_t CR16CNT;
uint32_t HH16CNT;
uint32_t HT16CNT;
uint32_t LT16CNT;
uint32_t MT16CNT;
uint32_t CH16CNT;
uint32_t OH16CNT;
uint32_t RD16CNT;
uint32_t RS16CNT;
uint32_t SD16CNT;
#define BD16LEN 3796UL
#define CP16LEN 4445UL
#define CR16LEN 48686UL
#define HH16LEN 1734UL
#define HT16LEN 5802UL
#define LT16LEN 7061UL
#define MT16LEN 7304UL
#define OH16LEN 4772UL
#define RD16LEN 52850UL这定义了示例计数器及其长度。为了保持示例引擎的运行,需要定义一个计算鼓声的函数。 uint16_t SYNTH909() {
int32_t DRUMTOTAL=0;
if (BD16CNT<BD16LEN) DRUMTOTAL+=(pgm_read_word_near(BD16 + BD16CNT++)^32768)-32768;
if (CP16CNT<CP16LEN) DRUMTOTAL+=(pgm_read_word_near(CP16 + CP16CNT++)^32768)-32768;
if (CR16CNT<CR16LEN) DRUMTOTAL+=(pgm_read_word_near(CR16 + CR16CNT++)^32768)-32768;
if (HH16CNT<HH16LEN) DRUMTOTAL+=(pgm_read_word_near(HH16 + HH16CNT++)^32768)-32768;
if (HT16CNT<HT16LEN) DRUMTOTAL+=(pgm_read_word_near(HT16 + HT16CNT++)^32768)-32768;
if (LT16CNT<LT16LEN) DRUMTOTAL+=(pgm_read_word_near(LT16 + LT16CNT++)^32768)-32768;
if (MT16CNT<MT16LEN) DRUMTOTAL+=(pgm_read_word_near(MT16 + MT16CNT++)^32768)-32768;
if (OH16CNT<OH16LEN) DRUMTOTAL+=(pgm_read_word_near(OH16 + OH16CNT++)^32768)-32768;
if (RD16CNT<RD16LEN) DRUMTOTAL+=(pgm_read_word_near(RD16 + RD16CNT++)^32768)-32768;
if (RS16CNT<RS16LEN) DRUMTOTAL+=(pgm_read_word_near(RS16 + RS16CNT++)^32768)-32768;
if (SD16CNT<SD16LEN) DRUMTOTAL+=(pgm_read_word_near(SD16 + SD16CNT++)^32768)-32768;
if (DRUMTOTAL>32767) DRUMTOTAL=32767;
if (DRUMTOTAL<-32767) DRUMTOTAL=-32767;
DRUMTOTAL+=32768;
return DRUMTOTAL;
} 在主循环中,我们添加了对示例引擎的调用。void loop() {
DAC=SYNTH909();
//Pulse Density Modulated 16-bit I2S DAC
for (uint8_t i=0;i<32;i++) {
i2sACC=i2sACC<<1;
if(DAC >= err) {
i2sACC|=1;
err += 0xFFFF-DAC;
}
else
{
err -= DAC;
}
}
bool flag=i2s_write_sample(i2sACC);最后是MIDI鼓的触发功能。void MidiNoteOn(uint8_t channel, uint8_t note, uint8_t velocity) {
/* 909 MIDI Triggers
Bass Drum MIDI-35
Bass Drum MIDI-36
Rim Shot MIDI-37
Snare Drum MIDI-38
Hand Clap MIDI-39
Snare Drum MIDI-40
Low Tom MIDI-41
Closed Hat MIDI-42
Low Tom MIDI-43
Closed Hat MIDI-44
Mid Tom MIDI-45
Open Hat MIDI-46
Mid Tom MIDI-47
Hi Tom MIDI-48
Crash Cymbal MIDI-49
Hi Tom MIDI-50
Ride Cymbal MIDI-51
*/
if (channel==10) {
if(note==35) BD16CNT=0;
if(note==36) BD16CNT=0;
if(note==37) RS16CNT=0;
if(note==38) SD16CNT=0;
if(note==39) CP16CNT=0;
if(note==40) SD16CNT=0;
if(note==41) LT16CNT=0;
if(note==42) HH16CNT=0;
if(note==43) LT16CNT=0;
if(note==44) HH16CNT=0;
if(note==45) MT16CNT=0;
if(note==46) OH16CNT=0;
if(note==47) MT16CNT=0;
if(note==48) HT16CNT=0;
if(note==49) CR16CNT=0;
if(note==50) HT16CNT=0;
if(note==51) RD16CNT=0;
}
}这方面的MIDI数据可以来自GPIO、串行MIDI或rtpMIDI上的边缘触发器。您可以很容易地添加速度数据,以缩放引擎中的样本,使重音鼓。重新排列ISR的代码
如果您有一个DMA,那么使用CPU填充DMA缓冲区并不好,如果您想运行MIDI输入也不太好。因此,我们将把代码重新排列到一个以2mS间隔服务的ISR中。除了添加了Ticker库之外,其他定义都是相同的。#include <Arduino.h>
#include "ESP8266WiFi.h"
#include <i2s.h>
#include <i2s_reg.h>
#include <pgmspace.h>
#include <Ticker.h>
uint32_t i2sACC;
uint8_t i2sCNT=32;
uint16_t DAC=0x8000;
uint16_t err;我们的测试正弦波形。int16_t sine = {
0x0000, 0x0324, 0x0647, 0x096a, 0x0c8b, 0x0fab, 0x12c8, 0x15e2,
0x18f8, 0x1c0b, 0x1f19, 0x2223, 0x2528, 0x2826, 0x2b1f, 0x2e11,
0x30fb, 0x33de, 0x36ba, 0x398c, 0x3c56, 0x3f17, 0x41ce, 0x447a,
0x471c, 0x49b4, 0x4c3f, 0x4ebf, 0x5133, 0x539b, 0x55f5, 0x5842,
0x5a82, 0x5cb4, 0x5ed7, 0x60ec, 0x62f2, 0x64e8, 0x66cf, 0x68a6,
0x6a6d, 0x6c24, 0x6dca, 0x6f5f, 0x70e2, 0x7255, 0x73b5, 0x7504,
0x7641, 0x776c, 0x7884, 0x798a, 0x7a7d, 0x7b5d, 0x7c29, 0x7ce3,
0x7d8a, 0x7e1d, 0x7e9d, 0x7f09, 0x7f62, 0x7fa7, 0x7fd8, 0x7ff6,
0x7fff, 0x7ff6, 0x7fd8, 0x7fa7, 0x7f62, 0x7f09, 0x7e9d, 0x7e1d,
0x7d8a, 0x7ce3, 0x7c29, 0x7b5d, 0x7a7d, 0x798a, 0x7884, 0x776c,
0x7641, 0x7504, 0x73b5, 0x7255, 0x70e2, 0x6f5f, 0x6dca, 0x6c24,
0x6a6d, 0x68a6, 0x66cf, 0x64e8, 0x62f2, 0x60ec, 0x5ed7, 0x5cb4,
0x5a82, 0x5842, 0x55f5, 0x539b, 0x5133, 0x4ebf, 0x4c3f, 0x49b4,
0x471c, 0x447a, 0x41ce, 0x3f17, 0x3c56, 0x398c, 0x36ba, 0x33de,
0x30fb, 0x2e11, 0x2b1f, 0x2826, 0x2528, 0x2223, 0x1f19, 0x1c0b,
0x18f8, 0x15e2, 0x12c8, 0x0fab, 0x0c8b, 0x096a, 0x0647, 0x0324,
0x0000, 0xfcdc, 0xf9b9, 0xf696, 0xf375, 0xf055, 0xed38, 0xea1e,
0xe708, 0xe3f5, 0xe0e7, 0xdddd, 0xdad8, 0xd7da, 0xd4e1, 0xd1ef,
0xcf05, 0xcc22, 0xc946, 0xc674, 0xc3aa, 0xc0e9, 0xbe32, 0xbb86,
0xb8e4, 0xb64c, 0xb3c1, 0xb141, 0xaecd, 0xac65, 0xaa0b, 0xa7be,
0xa57e, 0xa34c, 0xa129, 0x9f14, 0x9d0e, 0x9b18, 0x9931, 0x975a,
0x9593, 0x93dc, 0x9236, 0x90a1, 0x8f1e, 0x8dab, 0x8c4b, 0x8afc,
0x89bf, 0x8894, 0x877c, 0x8676, 0x8583, 0x84a3, 0x83d7, 0x831d,
0x8276, 0x81e3, 0x8163, 0x80f7, 0x809e, 0x8059, 0x8028, 0x800a,
0x8000, 0x800a, 0x8028, 0x8059, 0x809e, 0x80f7, 0x8163, 0x81e3,
0x8276, 0x831d, 0x83d7, 0x84a3, 0x8583, 0x8676, 0x877c, 0x8894,
0x89bf, 0x8afc, 0x8c4b, 0x8dab, 0x8f1e, 0x90a1, 0x9236, 0x93dc,
0x9593, 0x975a, 0x9931, 0x9b18, 0x9d0e, 0x9f14, 0xa129, 0xa34c,
0xa57e, 0xa7be, 0xaa0b, 0xac65, 0xaecd, 0xb141, 0xb3c1, 0xb64c,
0xb8e4, 0xbb86, 0xbe32, 0xc0e9, 0xc3aa, 0xc674, 0xc946, 0xcc22,
0xcf05, 0xd1ef, 0xd4e1, 0xd7da, 0xdad8, 0xdddd, 0xe0e7, 0xe3f5,
0xe708, 0xea1e, 0xed38, 0xf055, 0xf375, 0xf696, 0xf9b9, 0xfcdc
};
uint8_t phase=0; //Sine phase countersetup函数现在添加了一些Timer代码void setup() {
i2s_begin(); //Start the i2s DMA engine
i2s_set_rate(44100); //Set sample rate
pinMode(2, INPUT); //restore GPIOs taken by i2s
pinMode(15, INPUT);
timer1_attachInterrupt(onTimerISR); //Attach our sampling ISR
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE);
timer1_write(2000); //Service at 2mS intervall
}主循环现在是空的。void loop() {
}这是因为DMA引擎已经转移到ISR。void ICACHE_RAM_ATTR onTimerISR(){ //Code needs to be in IRAM because its a ISR
while (!(i2s_is_full())) { //Don’t block the ISR if the buffer is full
DAC=0x8000+sine;
//Pulse Density Modulated 16-bit I2S DAC
for (uint8_t i=0;i<32;i++) {
i2sACC=i2sACC<<1;
if(DAC >= err) {
i2sACC|=1;
err += 0xFFFF-DAC;
}
else
{
err -= DAC;
}
}
bool flag=i2s_write_sample(i2sACC);
}
timer1_write(2000);//Next in 2mS
}这与第一个例子的作用相同,但是您现在可以自由地在主循环中放入任何您喜欢的内容,因为计时器负责将数据加载到DMA。DMA以2mS的间隔自动服务,您可以在主循环中处理MIDI数据。读取串行MIDI数据我们如何读取MIDI数据呢?我们的串口被i2s流使用,所以不能用作串口。我们通过移动RX和TX引脚到备用引脚来做到这一点。Serial.swap();这将把RX引脚移动到GPIO13,将TX引脚移动到GPIO15。您需要在启动i2s引擎之前设置串口,因为串行设置将破坏i2s GPIO设置。void setup() {
Serial.begin(31250); //Start the serial port with default MIDI baudrate
Serial.swap(); //Move the TX and RX GPIOs to 15 and 13
i2s_begin(); //Start the i2s DMA engine
i2s_set_rate(44100); //Set sample rate
pinMode(2, INPUT); //restore GPIOs taken by i2s
pinMode(15, INPUT);
timer1_attachInterrupt(onTimerISR); //Attach our sampling ISR
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE);
timer1_write(2000); //Service at 2mS intervall
}添加MIDI流程定义。uint8_t MIDISTATE=0;
uint8_t MIDIRUNNINGSTATUS=0;
uint8_t MIDINOTE;
uint8_t MIDIVEL;还有MIDI处理器。void processMIDI(uint8_t MIDIRX) {
/*
Handling “Running status”
1.Buffer is cleared (ie, set to 0) at power up.
2.Buffer stores the status when a Voice Category Status (ie, 0x80 to 0xEF) is received.
3.Buffer is cleared when a System Common Category Status (ie, 0xF0 to 0xF7) is received.
4.Nothing is done to the buffer when a RealTime Category message is received.
5.Any data bytes are ignored when the buffer is 0.
*/
if ((MIDIRX>0xBF)&&(MIDIRX<0xF8)) {
MIDIRUNNINGSTATUS=0;
MIDISTATE=0;
return;
}
if (MIDIRX>0xF7) return;
if (MIDIRX & 0x80) {
MIDIRUNNINGSTATUS=MIDIRX;
MIDISTATE=1;
return;
}
if (MIDIRX < 0x80) {
if (!MIDIRUNNINGSTATUS) return;
if (MIDISTATE==1) {
MIDINOTE=MIDIRX;
MIDISTATE++;
return;
}
if (MIDISTATE==2) {
MIDIVEL=MIDIRX;
MIDISTATE=1;
//if (MIDIRUNNINGSTATUS==0x80) handleMIDInoteOFF(MIDIRUNNINGSTATUS,MIDINOTE,MIDIVEL);
//if (MIDIRUNNINGSTATUS==0x90) handleMIDInoteON(MIDIRUNNINGSTATUS,MIDINOTE,MIDIVEL);
//if (MIDIRUNNINGSTATUS==0xB0) handleMIDICC(MIDINOTE,MIDIVEL);
}
}
}您需要为noteOFF、noteON和MIDICC添加处理程序。现在我们可以在主循环中处理传入的MIDI字节。void loop() {
if (Serial.available()) processMIDI(Serial.read());
}现在,您可以将我们新的DMA引擎和串行MIDI处理器应用到简单的鼓播放器上,并从键盘或音序器播放它。在ESP8266上安装rtpMIDItpMIDI或者Apple-MIDI over WiFI怎么样?它在我们的鼓机上运行得很好。要使用它,您需要下载并安装Apple-MIDI库:https://github.com/lathoub/Arduino-AppleMIDI-Library。#include <Arduino.h>
#include "ESP8266WiFi.h"
#include <WiFiClient.h>
#include <WiFiUdp.h>
#include <i2s.h>
#include <i2s_reg.h>
#include <pgmspace.h>
#include "AppleMidi.h"
#include <Ticker.h>
extern “C” {
#include “user_interface.h”
}
char ssid[] = "YourSSID"; //your network SSID (name)
char pass[] = "YourKEY"; // your network password (use for WPA, or use as key for WEP)
APPLEMIDI_CREATE_INSTANCE(WiFiUDP, AppleMIDI); // see definition in AppleMidi_Defs.h
// Forward declaration
void OnAppleMidiConnected(uint32_t ssrc, char* name);
void OnAppleMidiDisconnected(uint32_t ssrc);
void OnAppleMidiNoteOn(byte channel, byte note, byte velocity);
void OnAppleMidiNoteOff(byte channel, byte note, byte velocity);
uint32_t i2sACC;
uint8_t i2sCNT=32;
uint16_t DAC=0x8000;
uint16_t err;
uint32_t BD16CNT;
uint32_t CP16CNT;
uint32_t CR16CNT;
uint32_t HH16CNT;
uint32_t HT16CNT;
uint32_t LT16CNT;
uint32_t MT16CNT;
uint32_t CH16CNT;
uint32_t OH16CNT;
uint32_t RD16CNT;
uint32_t RS16CNT;
uint32_t SD16CNT;
#define BD16LEN 3796UL
#define CP16LEN 4445UL
#define CR16LEN 48686UL
#define HH16LEN 1734UL
#define HT16LEN 5802UL
#define LT16LEN 7061UL
#define MT16LEN 7304UL
#define OH16LEN 4772UL
#define RD16LEN 52850UL
#define RS16LEN 1316UL
#define SD16LEN 5577UL
const uint16_t BD16 PROGMEM = {
40, 85, 137, 144, -30, -347, -609, -785, // 0-7上面的定义与原始的鼓采样器代码相同,但添加了Apple MIDI。909合成引擎还是一样的。uint16_t SYNTH909() {
int32_t DRUMTOTAL=0;
if (BD16CNT<BD16LEN) DRUMTOTAL+=(pgm_read_word_near(BD16 + BD16CNT++)^32768)-32768;
if (CP16CNT<CP16LEN) DRUMTOTAL+=(pgm_read_word_near(CP16 + CP16CNT++)^32768)-32768;
if (CR16CNT<CR16LEN) DRUMTOTAL+=(pgm_read_word_near(CR16 + CR16CNT++)^32768)-32768;
if (HH16CNT<HH16LEN) DRUMTOTAL+=(pgm_read_word_near(HH16 + HH16CNT++)^32768)-32768;
if (HT16CNT<HT16LEN) DRUMTOTAL+=(pgm_read_word_near(HT16 + HT16CNT++)^32768)-32768;
if (LT16CNT<LT16LEN) DRUMTOTAL+=(pgm_read_word_near(LT16 + LT16CNT++)^32768)-32768;
if (MT16CNT<MT16LEN) DRUMTOTAL+=(pgm_read_word_near(MT16 + MT16CNT++)^32768)-32768;
if (OH16CNT<OH16LEN) DRUMTOTAL+=(pgm_read_word_near(OH16 + OH16CNT++)^32768)-32768;
if (RD16CNT<RD16LEN) DRUMTOTAL+=(pgm_read_word_near(RD16 + RD16CNT++)^32768)-32768;
if (RS16CNT<RS16LEN) DRUMTOTAL+=(pgm_read_word_near(RS16 + RS16CNT++)^32768)-32768;
if (SD16CNT<SD16LEN) DRUMTOTAL+=(pgm_read_word_near(SD16 + SD16CNT++)^32768)-32768;
if(DRUMTOTAL>32767) DRUMTOTAL=32767;
if(DRUMTOTAL<-32767) DRUMTOTAL=-32767;
DRUMTOTAL+=32768;
return DRUMTOTAL;
}安装程序包括一些新代码,以添加ESP8266到您的WiFi网络。void setup() {
//WiFi.forceSleepBegin();
//delay(1);
system_update_cpu_freq(160);
//Serial.begin(9600);
WiFi.begin(ssid, pass);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
//Serial.print(F(“IP address is “));
//Serial.println(WiFi.localIP());
AppleMIDI.begin(“ESP909”); // ‘ESP909’ will show up as the session name
AppleMIDI.OnReceiveNoteOn(OnAppleMidiNoteOn);
i2s_begin();
i2s_set_rate(44100);
timer1_attachInterrupt(onTimerISR); //Attach our sampling ISR
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE);
timer1_write(2000); //Service at 2mS intervall
}主循环中现在有Apple MIDI状态码。void loop() {
AppleMIDI.run();
}主采样ISR是一样的。void ICACHE_RAM_ATTR onTimerISR(){
while (!(i2s_is_full())) { //Don't block the ISR
DAC=SYNTH909();
//----------------- Pulse Density Modulated 16-bit I2S DAC --------------------
for (uint8_t i=0;i<32;i++) {
i2sACC=i2sACC<<1;
if(DAC >= err) {
i2sACC|=1;
err += 0xFFFF-DAC;
}
else
{
err -= DAC;
}
}
bool flag=i2s_write_sample(i2sACC);
//-----------------------------------------------------------------------
}
timer1_write(2000);//Next in 2mS
} 但是现在增加了一个处理MIDI事件的新函数。void OnAppleMidiNoteOn(byte channel, byte note, byte velocity) {
/* Triggers
Bass Drum MIDI-35
Bass Drum MIDI-36
Rim Shot MIDI-37
Snare Drum MIDI-38
Hand Clap MIDI-39
Snare Drum MIDI-40
Low Tom MIDI-41
Closed Hat MIDI-42
Low Tom MIDI-43
Closed Hat MIDI-44
Mid Tom MIDI-45
Open Hat MIDI-46
Mid Tom MIDI-47
Hi Tom MIDI-48
Crash Cymbal MIDI-49
Hi Tom MIDI-50
Ride Cymbal MIDI-51
*/
if (channel==10) {
if(note==35) BD16CNT=0;
if(note==36) BD16CNT=0;
if(note==37) RS16CNT=0;
if(note==38) SD16CNT=0;
if(note==39) CP16CNT=0;
if(note==40) SD16CNT=0;
if(note==41) LT16CNT=0;
if(note==42) HH16CNT=0;
if(note==43) LT16CNT=0;
if(note==44) HH16CNT=0;
if(note==45) MT16CNT=0;
if(note==46) OH16CNT=0;
if(note==47) MT16CNT=0;
if(note==48) HT16CNT=0;
if(note==49) CR16CNT=0;
if(note==50) HT16CNT=0;
if(note==51) RD16CNT=0;
}
}要做到这一点,你需要在你的Mac、iPad或PC上设置Apple rtpMIDI。我不能向您展示如何做到这一点,因为如何做到这一点取决于您的平台。您需要找到ESP8266的IP地址,以便将其与MIDI计算机配对。启用串行调试代码并查看您的IP地址的串行控制台。但一旦完成,它就会运行得很好。这是最基本的样本播放器。只要样品适合你的闪存空间,你可以播放任何你想要的,并通过MIDI或WiFi控制它。与90年代的采样器相比,这要好多了。在这么小的空间里,16位音频和4兆字节的内存真是太好了。样玩键盘随着我们对ESP8266的新发现的知识,我们现在继续创建一个示例演奏键盘或Rompler。与我们的鼓采样器不同的是,它播放的样本是彩色和多音色的。上图中的EMU-II使用了8位DPCM采样,奇怪的采样率为27.7KHz。为了使它更简单,我们将以32KHz的速率使用16位有符号采样。EMU-II是8声道复调,但我不擅长写语音分配器,所以我们的采样器将是128声道全复调。虽然理论上你可以用单个信封一次性播放所有128个MIDI键,但由于处理能力的限制,复调将会减少。我们的定义#include <Arduino.h>
#include "ESP8266WiFi.h"
#include <i2s.h>
#include <i2s_reg.h>
#include <pgmspace.h>
#include <Ticker.h>
uint32_t i2sACC;
uint8_t i2sCNT=32;
uint16_t DAC=0x8000;
uint16_t err;
//Envelope and VCA parameters
volatile ENVcnt=8; //16mS env resolution
int16_t VCA; //VCA levels
volatile uint8_t ATTACK=30; // ENV Attack rate 1-255
volatile uint8_t RELEASE=3; // ENV Release rate 1-255
//Sample parameters and tables
uint32_t FREQ; //Phase accumulators
uint32_t SPNT; //Sample pointers
uint32_t LOOP1; //Start of loop segment in sample
uint32_t LOOP2; //End of loop segment in sample
uint32_t SLEN; //Length of sample 正如您所看到的,每个参数有128个表。键盘上每个键的参数为:
[*]相位累加器(后面详细解释)
[*]采样指针(采样内时间的线性计数器)
[*]LOOP1(维持循环的起点)
[*]LOOP2(循环的结束点,它在这里跳回到LOOP1)
[*]长度(总样本的长度或字数)
设置
这是我们使用MIDI输入来播放示例的设置例程。void setup() {
WiFi.forceSleepBegin(); //Turn off WiFi radio
delay(1); //Wait for it to turn off
system_update_cpu_freq(160);
Serial.begin(31250); //Start the serial port with default MIDI baudrate
Serial.swap(); //Move the TX and RX GPIOs to 15 and 13
i2s_begin(); //Start the i2s DMA engine
i2s_set_rate(32000); //Set sample rate
pinMode(2, INPUT); //restore GPIOs taken by i2s
pinMode(15, INPUT);
timer1_attachInterrupt(onTimerISR); //Attach our sampling ISR
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE);
timer1_write(2000); //Service at 2mS intervall
}它会关闭WiFi收音机,将CPU频率提高到160MHz,为MIDI设置UART,启动i2s DMA引擎并打开Timer。我们还需要Timer中断,它负责以2mS的间隔加载DMA。void ICACHE_RAM_ATTR onTimerISR(){ //Code needs to be in IRAM because its a ISR
while (!(i2s_is_full())) { //Don’t block the ISR if the buffer is full
DAC=samplerTick(); //Calculate current sample value
//Pulse Density Modulated 16-bit I2S DAC
for (uint8_t i=0;i<32;i++) {
i2sACC=i2sACC<<1;
if(DAC >= err) {
i2sACC|=1;
err += 0xFFFF-DAC;
}
else
{
err -= DAC;
}
}
bool flag=i2s_write_sample(i2sACC);
}
//Envelope handler
if (!(ENVcnt--)) { //Calculate ENV every 16mS
ENVcnt==8;
for (envcnt=0;envcnt<128;envcnt++) { //128 VCA's
if ((MIDItable>0)&&(VCA<255)) {
VCA+=ATTACK;
if (VCA>255) VCA=255;
}
if ((MIDItable==0)&&(VCA>0)) {
VCA-=RELEASE;
if (VCA<0) VCA=0;
}
}
timer1_write(2000);//Next in 2mS
}在Timer处理程序内部,我们还以16mS的间隔运行信封生成器。每个键都有自己的攻击/衰减音量信封。MIDI处理程序
我们的loop()负责检查串行数据是否可用,如果可用则运行MIDI处理器。void loop() {
if (Serial.available()) processMIDI(Serial.read());
}如果MIDI数据可用,它将对其进行处理。void processMIDI(uint8_t MIDIRX) { //MIDI processor
/*
Handling “Running status”
1.Buffer is cleared (ie, set to 0) at power up.
2.Buffer stores the status when a Voice Category Status (ie, 0x80 to 0xEF) is received.
3.Buffer is cleared when a System Common Category Status (ie, 0xF0 to 0xF7) is received.
4.Nothing is done to the buffer when a RealTime Category message is received.
5.Any data bytes are ignored when the buffer is 0.
*/
if ((MIDIRX>0xBF)&&(MIDIRX<0xF8)) {
MIDIRUNNINGSTATUS=0;
MIDISTATE=0;
return;
}
if (MIDIRX>0xF7) return;
if (MIDIRX & 0x80) {
MIDIRUNNINGSTATUS=MIDIRX;
MIDISTATE=1;
return;
}
if (MIDIRX < 0x80) {
if (!MIDIRUNNINGSTATUS) return;
if (MIDISTATE==1) {
MIDINOTE=MIDIRX;
MIDISTATE++;
return;
}
if (MIDISTATE==2) {
MIDIVEL=MIDIRX;
MIDISTATE=1;
if (MIDIRUNNINGSTATUS==0x80) handleMIDInoteOFF(MIDIRUNNINGSTATUS,MIDINOTE,MIDIVEL);
if (MIDIRUNNINGSTATUS==0x90) handleMIDInoteON(MIDIRUNNINGSTATUS,MIDINOTE,MIDIVEL);
//if (MIDIRUNNINGSTATUS==0xB0) handleMIDICC(MIDINOTE,MIDIVEL);
}
}
}
void handleMIDInoteON(MIDIRUNNINGSTATUS,MIDINOTE,MIDIVEL);
MIDItable=MIDIVEL;
}
void handleMIDInoteOFF(MIDIRUNNINGSTATUS,MIDINOTE,MIDIVEL);
MIDItable=0;
}handleMIDInoteON/OFF写入MIDI映射表,显示哪些键被按下。样本引擎
这里处理所有的样本计数器,并对所有不同的样本进行循环和求和。由于每个键的频率与下一个键的十二分根关系或乘/除1.05,我们如何得到每个键的频率,因为它们都以32KHz处理?答案是相位累加器。它实际上计算了一个样本蜱虫的分数。计数器是15位的,对于最高的键,我们添加0x80000000,如果它溢出,我们有一个完整的tick。对于低于一个八度的音阶,我们添加0x40000000,在溢出时我们有一半的频率。下面是0x80000000 / 1.05等等。C3八度的节拍是这样的:void samplerTick() //Calculate total sample value for each playing note
int32_t total=0;
if ((VCA)&&(SPNT<SLEN)) { //If VCA is active and the sample has not reached end
FREQ+=1073741824; //Add frequency to the phase accumulator for C3 key
if (FREQ&0x8000000) { //If phase accumulator overflows
FREQ&=0x7FFFFFFF; //Trim off MSB
if ((SPNT>LOOP2)&&(MIDItable)) SPNT=LOOP1; //Check if we're in a loop
total+=(((pgm_read_word_near(SAMPLE + SPNT)^32768)-32768)*VCA)>>8; //Add the sample value to total with ENV scaling
SPNT++; //Increment sample pointer
}
}
if ((VCA)&&(SPNT<SLEN)) {
FREQ+=1137589835; //Add frequency to counter for C3# key
if (FREQ&0x8000000) {
FREQ&=0x7FFFFFFF;
if ((SPNT>LOOP2)&&(MIDItable)) SPNT=LOOP1;
total+=(((pgm_read_word_near(SAMPLE + SPNT)^32768)-32768)*VCA)>>8;
SPNT++;
}
}
if ((VCA)&&(SPNT<SLEN)) {
FREQ+=1205234447; //Add frequency to counter for D3 key
if (FREQ&0x8000000) {
FREQ&=0x7FFFFFFF;
if ((SPNT>LOOP2)&&(MIDItable)) SPNT=LOOP1;
total+=(((pgm_read_word_near(SAMPLE + SPNT)^32768)-32768)*VCA)>>8;
SPNT++;
}
}
if ((VCA)&&(SPNT<SLEN)) {
FREQ+=1276901416; //Add frequency to counter for D3# key
if (FREQ&0x8000000) {
FREQ&=0x7FFFFFFF;
if ((SPNT>LOOP2)&&(MIDItable)) SPNT=LOOP1;
total+=(((pgm_read_word_near(SAMPLE + SPNT)^32768)-32768)*VCA)>>8;
SPNT++;
}
}
if ((VCA)&&(SPNT<SLEN)) {
FREQ+=1352829926; //Add frequency to counter for E3 key
if (FREQ&0x8000000) {
FREQ&=0x7FFFFFFF;
if ((SPNT>LOOP2)&&(MIDItable)) SPNT=LOOP1;
total+=(((pgm_read_word_near(SAMPLE + SPNT)^32768)-32768)*VCA)>>8;
SPNT++;
}
}
if ((VCA)&&(SPNT<SLEN)) {
FREQ+=1433273379; //Add frequency to counter for F3 key
if (FREQ&0x8000000) {
FREQ&=0x7FFFFFFF;
if ((SPNT>LOOP2)&&(MIDItable)) SPNT=LOOP1;
total+=(((pgm_read_word_near(SAMPLE + SPNT)^32768)-32768)*VCA)>>8;
SPNT++;
}
}
if ((VCA)&&(SPNT<SLEN)) {
FREQ+=1518500249; //Add frequency to counter for G3 key
if (FREQ&0x8000000) {
FREQ&=0x7FFFFFFF;
if ((SPNT>LOOP2)&&(MIDItable)) SPNT=LOOP1;
total+=(((pgm_read_word_near(SAMPLE + SPNT)^32768)-32768)*VCA)>>8;
SPNT++;
}
}
if ((VCA)&&(SPNT<SLEN)) {
FREQ+=1608794973; //Add frequency to counter for G3# key
if (FREQ&0x8000000) {
FREQ&=0x7FFFFFFF;
if ((SPNT>LOOP2)&&(MIDItable)) SPNT=LOOP1;
total+=(((pgm_read_word_near(SAMPLE + SPNT)^32768)-32768)*VCA)>>8;
SPNT++;
}
}
if ((VCA)&&(SPNT<SLEN)) {
FREQ+=1704458900; //Add frequency to counter for A3 key
if (FREQ&0x8000000) {
FREQ&=0x7FFFFFFF;
if ((SPNT>LOOP2)&&(MIDItable)) SPNT=LOOP1;
total+=(((pgm_read_word_near(SAMPLE + SPNT)^32768)-32768)*VCA)>>8;
SPNT++;
}
}
if ((VCA)&&(SPNT<SLEN)) {
FREQ+=1805811301; //Add frequency to counter for A3# key
if (FREQ&0x8000000) {
FREQ&=0x7FFFFFFF;
if ((SPNT>LOOP2)&&(MIDItable)) SPNT=LOOP1;
total+=(((pgm_read_word_near(SAMPLE + SPNT)^32768)-32768)*VCA)>>8;
SPNT++;
}
}
if ((VCA)&&(SPNT<SLEN)) {
FREQ+=1913190429; //Add frequency to counter for B3 key
if (FREQ&0x8000000) {
FREQ&=0x7FFFFFFF;
if ((SPNT>LOOP2)&&(MIDItable)) SPNT=LOOP1;
total+=(((pgm_read_word_near(SAMPLE + SPNT)^32768)-32768)*VCA)>>8;
SPNT++;
}
}
if ((VCA)&&(SPNT<SLEN)) {
FREQ+=2026954652; //Add frequency to counter for B3# key
if (FREQ&0x8000000) {
FREQ&=0x7FFFFFFF;
if ((SPNT>LOOP2)&&(MIDItable)) SPNT=LOOP1;
total+=(((pgm_read_word_near(SAMPLE + SPNT)^32768)-32768)*VCA)>>8;
SPNT++;
}
}
if ((VCA)&&(SPNT<SLEN)) {
FREQ+=2147483648; //Add frequency to counter for C4 key, this overflows every tick thus 32KHz
if (FREQ&0x8000000) {
FREQ&=0x7FFFFFFF;
if ((SPNT>LOOP2)&&(MIDItable)) SPNT=LOOP1;
total+=(((pgm_read_word_near(SAMPLE + SPNT)^32768)-32768)*VCA)>>8;
SPNT++;
}
}
if (total>32767) total=32767; //Clip to max
if (total<-32767) total=-32767; //Clip to min
total+=32768; //Center value
return total;
}对于八度音阶中的每个键,如果它仍然在发声(VCA>0),并且样本还没有结束,我们将该频率添加到该键的相位累加器中。然后检查键是否被持有,并传递一个循环点,在这种情况下,将示例指针跳到循环的开头。接着我们获取样本值并将其体积缩放到VCA值。我们可以有多个样本因为每个键都有唯一的样本参数。最后,由于我们在增加样本,我们可以传递信号的动态范围,所以我们将它剪辑到极限。我展示了C3八度的例子,因为C4是32KHz或一个节拍和我们制作样本的键。如果您对此项目有任何想法、意见或问题,请在下方留言。
页:
[1]