ESP8266
ESP8266是相当强大的音频应用与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[phase++]);
}
正弦波数据:
int16_t sine[256] = {
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[3796] PROGMEM = {
40, 85, 137, 144, -30, -347, -609, -785, // 0-7
const uint16_t CP16[4445] 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[256] = {
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 counter
setup函数现在添加了一些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[phase++];
//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上安装rtpMIDI
tpMIDI或者Apple-MIDI over WiFI怎么样?
它在我们的鼓机上运行得很好。
#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[3796] 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[128]; //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[128]; //Phase accumulators
uint32_t SPNT[128]; //Sample pointers
uint32_t LOOP1[128]; //Start of loop segment in sample
uint32_t LOOP2[128]; //End of loop segment in sample
uint32_t SLEN[128]; //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[envcnt]>0)&&(VCA[envcnt]<255)) {
VCA[envcnt]+=ATTACK;
if (VCA[envcnt]>255) VCA[envcnt]=255;
}
if ((MIDItable[envcnt]==0)&&(VCA[envcnt]>0)) {
VCA[envcnt]-=RELEASE;
if (VCA[envcnt]<0) VCA[envcnt]=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[MIDINOTE]=MIDIVEL;
}
void handleMIDInoteOFF(MIDIRUNNINGSTATUS,MIDINOTE,MIDIVEL);
MIDItable[MIDINOTE]=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[48+0])&&(SPNT[48+0]<SLEN[48+0])) { //If VCA is active and the sample has not reached end
FREQ[48+0]+=1073741824; //Add frequency to the phase accumulator for C3 key
if (FREQ[48+0]&0x8000000) { //If phase accumulator overflows
FREQ[48+0]&=0x7FFFFFFF; //Trim off MSB
if ((SPNT[48+0]>LOOP2[48+0])&&(MIDItable[48+0])) SPNT[48+0]=LOOP1[48+0]; //Check if we're in a loop
total+=(((pgm_read_word_near(SAMPLE + SPNT[48+0])^32768)-32768)*VCA[48+0])>>8; //Add the sample value to total with ENV scaling
SPNT[48+0]++; //Increment sample pointer
}
}
if ((VCA[49+0])&&(SPNT[49+0]<SLEN[49+0])) {
FREQ[49+0]+=1137589835; //Add frequency to counter for C3# key
if (FREQ[49+0]&0x8000000) {
FREQ[49+0]&=0x7FFFFFFF;
if ((SPNT[49+0]>LOOP2[49+0])&&(MIDItable[49+0])) SPNT[49+0]=LOOP1[49+0];
total+=(((pgm_read_word_near(SAMPLE + SPNT[49+0])^32768)-32768)*VCA[49+0])>>8;
SPNT[49+0]++;
}
}
if ((VCA[50+0])&&(SPNT[50+0]<SLEN[50+0])) {
FREQ[50+0]+=1205234447; //Add frequency to counter for D3 key
if (FREQ[50+0]&0x8000000) {
FREQ[50+0]&=0x7FFFFFFF;
if ((SPNT[50+0]>LOOP2[50+0])&&(MIDItable[50+0])) SPNT[50+0]=LOOP1[50+0];
total+=(((pgm_read_word_near(SAMPLE + SPNT[50+0])^32768)-32768)*VCA[50+0])>>8;
SPNT[50+0]++;
}
}
if ((VCA[51+0])&&(SPNT[51+0]<SLEN[51+0])) {
FREQ[51+0]+=1276901416; //Add frequency to counter for D3# key
if (FREQ[51+0]&0x8000000) {
FREQ[51+0]&=0x7FFFFFFF;
if ((SPNT[51+0]>LOOP2[51+0])&&(MIDItable[51+0])) SPNT[51+0]=LOOP1[51+0];
total+=(((pgm_read_word_near(SAMPLE + SPNT[51+0])^32768)-32768)*VCA[51+0])>>8;
SPNT[51+0]++;
}
}
if ((VCA[52+0])&&(SPNT[52+0]<SLEN[52+0])) {
FREQ[52+0]+=1352829926; //Add frequency to counter for E3 key
if (FREQ[52+0]&0x8000000) {
FREQ[52+0]&=0x7FFFFFFF;
if ((SPNT[52+0]>LOOP2[52+0])&&(MIDItable[52+0])) SPNT[52+0]=LOOP1[52+0];
total+=(((pgm_read_word_near(SAMPLE + SPNT[52+0])^32768)-32768)*VCA[52+0])>>8;
SPNT[52+0]++;
}
}
if ((VCA[53+0])&&(SPNT[53+0]<SLEN[53+0])) {
FREQ[53+0]+=1433273379; //Add frequency to counter for F3 key
if (FREQ[53+0]&0x8000000) {
FREQ[53+0]&=0x7FFFFFFF;
if ((SPNT[53+0]>LOOP2[53+0])&&(MIDItable[53+0])) SPNT[53+0]=LOOP1[53+0];
total+=(((pgm_read_word_near(SAMPLE + SPNT[53+0])^32768)-32768)*VCA[53+0])>>8;
SPNT[53+0]++;
}
}
if ((VCA[54+0])&&(SPNT[54+0]<SLEN[54+0])) {
FREQ[54+0]+=1518500249; //Add frequency to counter for G3 key
if (FREQ[54+0]&0x8000000) {
FREQ[54+0]&=0x7FFFFFFF;
if ((SPNT[54+0]>LOOP2[54+0])&&(MIDItable[54+0])) SPNT[54+0]=LOOP1[54+0];
total+=(((pgm_read_word_near(SAMPLE + SPNT[54+0])^32768)-32768)*VCA[54+0])>>8;
SPNT[54+0]++;
}
}
if ((VCA[55+0])&&(SPNT[55+0]<SLEN[55+0])) {
FREQ[55+0]+=1608794973; //Add frequency to counter for G3# key
if (FREQ[55+0]&0x8000000) {
FREQ[55+0]&=0x7FFFFFFF;
if ((SPNT[55+0]>LOOP2[55+0])&&(MIDItable[55+0])) SPNT[55+0]=LOOP1[55+0];
total+=(((pgm_read_word_near(SAMPLE + SPNT[55+0])^32768)-32768)*VCA[55+0])>>8;
SPNT[55+0]++;
}
}
if ((VCA[56+0])&&(SPNT[56+0]<SLEN[56+0])) {
FREQ[56+0]+=1704458900; //Add frequency to counter for A3 key
if (FREQ[56+0]&0x8000000) {
FREQ[56+0]&=0x7FFFFFFF;
if ((SPNT[56+0]>LOOP2[56+0])&&(MIDItable[56+0])) SPNT[56+0]=LOOP1[56+0];
total+=(((pgm_read_word_near(SAMPLE + SPNT[56+0])^32768)-32768)*VCA[56+0])>>8;
SPNT[56+0]++;
}
}
if ((VCA[57+0])&&(SPNT[57+0]<SLEN[57+0])) {
FREQ[57+0]+=1805811301; //Add frequency to counter for A3# key
if (FREQ[57+0]&0x8000000) {
FREQ[57+0]&=0x7FFFFFFF;
if ((SPNT[57+0]>LOOP2[57+0])&&(MIDItable[57+0])) SPNT[57+0]=LOOP1[57+0];
total+=(((pgm_read_word_near(SAMPLE + SPNT[57+0])^32768)-32768)*VCA[57+0])>>8;
SPNT[57+0]++;
}
}
if ((VCA[58+0])&&(SPNT[58+0]<SLEN[58+0])) {
FREQ[58+0]+=1913190429; //Add frequency to counter for B3 key
if (FREQ[58+0]&0x8000000) {
FREQ[58+0]&=0x7FFFFFFF;
if ((SPNT[58+0]>LOOP2[58+0])&&(MIDItable[58+0])) SPNT[58+0]=LOOP1[58+0];
total+=(((pgm_read_word_near(SAMPLE + SPNT[58+0])^32768)-32768)*VCA[58+0])>>8;
SPNT[58+0]++;
}
}
if ((VCA[59+0])&&(SPNT[59+0]<SLEN[59+0])) {
FREQ[59+0]+=2026954652; //Add frequency to counter for B3# key
if (FREQ[59+0]&0x8000000) {
FREQ[59+0]&=0x7FFFFFFF;
if ((SPNT[59+0]>LOOP2[59+0])&&(MIDItable[59+0])) SPNT[59+0]=LOOP1[59+0];
total+=(((pgm_read_word_near(SAMPLE + SPNT[59+0])^32768)-32768)*VCA[59+0])>>8;
SPNT[59+0]++;
}
}
if ((VCA[60+0])&&(SPNT[60+0]<SLEN[60+0])) {
FREQ[60+0]+=2147483648; //Add frequency to counter for C4 key, this overflows every tick thus 32KHz
if (FREQ[60+0]&0x8000000) {
FREQ[60+0]&=0x7FFFFFFF;
if ((SPNT[60+0]>LOOP2[60+0])&&(MIDItable[60+0])) SPNT[60+0]=LOOP1[60+0];
total+=(((pgm_read_word_near(SAMPLE + SPNT[60+0])^32768)-32768)*VCA[60+0])>>8;
SPNT[60+0]++;
}
}
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或一个节拍和我们制作样本的键。
如果您对此项目有任何想法、意见或问题,请在下方留言。