亲,“电路城”已合并升级到更全、更大、更强的「新与非网」。点击查看「新与非网」

本网页已闲置超过3分钟,按键盘任意键或点击空白处,即可回到网页

基于ESP8266的鼓采样播放器

发布时间:2022-10-20
分享到:

基于ESP8266的鼓采样播放器

发布时间:2022-10-20
分享到:

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怎么样?

它在我们的鼓机上运行得很好。

要使用它,您需要下载并安装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[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或一个节拍和我们制作样本的键。

如果您对此项目有任何想法、意见或问题,请在下方留言。

以上内容翻译自网络,原作者:,如涉及侵权,可联系删除。

加入微信技术交流群

技术交流,职业进阶

关注与非网服务号

获取电子工程师福利

加入电路城 QQ 交流群

与技术大牛交朋友

讨论