aboutsummaryrefslogtreecommitdiff
path: root/esp32/lego-piano.ino
blob: 6a8ff0993f6648fbb975a027c42ac5cf8418f018 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
/*
  matrix-scan a set of QRD1114 or similar, each maps to a note, play
  those notes via a soundfont
 
 */
 
#include <driver/dac.h>
#include <driver/i2s.h>
 
#define TSF_IMPLEMENTATION
#define TSF_NO_STDIO
#include "../ESP8266Audio/src/libtinysoundfont/tsf.h"
 
#include "../RingBuffer/src/RingBuf.h"
 
// this is the soundfont, we embed it in the program's memory 
#include "font.h"
 
#define DEBUG 0
 
// how many rows & columns does the matrix have? 
const size_t row_count = 5;
const size_t col_count = 5;
 
/*
  which pins to use
 
  each "row" pins goes to a current-limiting resistor (~220Ω) and then
  to the LED anodes, also to a pull-up resistor (~10kΩ) and then to
  the transistor collectors
 
  each "column" pin goes to LED cathodes and transistor emitters
 
  each "adc" pin goes between pull-up and collector
*/
const int rows[row_count] = { 523191826 };
const int cols[col_count] = { 1733162122 };
const int adc[row_count] = { 24122714 };
// this turns on the amplifier 
const int ampEnable = 32;
 
// sample rate to render the soundfont at; higher means theoretically 
// better quality, but slows down everything, so not worth it 
const int sampleRate = 11025;
 
// nothing to customise below this point 
 
const i2s_port_t i2sPort = I2S_NUM_0;
 
// state of the matrix 
char pressed[row_count*col_count] = { 0 };
int currentSensor = 0;
 
// buffers to exchange data between soundfont renderer and I2S driver 
const size_t bufferSize = 1000;
RingBuf<short, bufferSize> buffer;
short intermediateBuffer[bufferSize];
 
// the soundfont renderer 
tsf* g_TinySoundFont = 0;
 
void setup() {
  // for debugging 
  Serial.begin(115200);
 
  // turn the amplifier off while we set things up 
  pinMode(ampEnable, OUTPUT);
  digitalWrite(ampEnable, LOW);
 
  // set up the DAC and the I2S driver 
  dac_i2s_enable();
  dac_output_enable(DAC_CHANNEL_1);
 
  // 16-bit single channel I2S using the internal DAC 
  static const i2s_config_t i2s_config = {
      .mode = (i2s_mode_t)( I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN ),
      .sample_rate = sampleRate,
      .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
      .channel_format = I2S_CHANNEL_FMT_ALL_RIGHT,
      .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // high interrupt priority 
      .dma_buf_count = 8,
      .dma_buf_len = 64,
      .use_apll = false
  };
  i2s_driver_install(i2sPort, &i2s_config, 0NULL);
  // only dac1: we don't need stereo, also we're using the dac2 pin as 
  // GPIO 
  i2s_set_dac_mode(I2S_DAC_CHANNEL_RIGHT_EN);
 
  // set pins' role 
  for (int i=0;i<row_count;++i) {
    pinMode(rows[i], OUTPUT|PULLUP);
    pinMode(adc[i], INPUT);
  }
  for (int i=0;i<col_count;++i) {
    pinMode(cols[i], OUTPUT|PULLDOWN);
  }
 
  currentSensor = 0;
 
  // load the soundfont  
  g_TinySoundFont = tsf_load_memory(SoundFont, sizeof(SoundFont));
  if (g_TinySoundFont) {
    tsf_set_output(g_TinySoundFont, TSF_MONO, sampleRate, 0);
  }
  else {
    Serial.println("failed to start tsf");
  }
 
  // ok, turn the amplifier on now 
  digitalWrite(ampEnable, HIGH);
}
 
/**** Pin handling functions
 */
 
// set a column pin to "high impedance" 
void tristate(int pin) {
  pinMode(pin,OUTPUT|PULLDOWN);
  digitalWrite(pin,LOW);
  pinMode(pin,INPUT);
}
 
// set a row pin to 3.3V 
void power(int pin) {
  pinMode(pin,OUTPUT|PULLUP);
  digitalWrite(pin,HIGH);
}
 
// set a row or column pin to ground 
void ground(int pin) {
  pinMode(pin,OUTPUT|PULLDOWN);
  digitalWrite(pin,LOW);
}
 
// turn a sensor on 
void enableSensor(int sensor) {
  int row = (sensor/col_count) % row_count ;
  int col = sensor%col_count;
 
#if DEBUG & 0x02
  Serial.print("enabling ");
  Serial.print(row);
  Serial.print(" ");
  Serial.println(col);
#endif
 
  for (int i=0;i<row_count;++i) {
    if (i==row) { power(rows[i]); }
    else { ground(rows[i]); }
  }
  for (int i=0;i<col_count;++i) {
    if (i==col) { ground(cols[i]); }
    else { tristate(cols[i]); }
  }
}
 
// read a sensor 
int sense(int sensor) {
  int row = (sensor/col_count)%row_count;
 
  int value = analogRead(adc[row]);
 
#if DEBUG & 0x04
  Serial.println(value);
#endif
 
  return value < 500;
}
 
void loop() {
#if DEBUG & 0x01
  Serial.print("current sensor ");
  Serial.println(currentSensor);
#endif
 
  enableSensor(currentSensor);
  unsigned long t = millis();
 
  // the sensor needs at least a millisecond to see anything, let's do 
  // work in the meantime 
 
  size_t written=100;
  short value;
  // fill the I2S buffer from our ringbuffer; the I2S API doesn't tell 
  // us how much space it has in its internal buffers, so we must send 
  // one short at a time, without blocking, until it fails (which 
  // means the buffers are full) 
  while (written > 0{
    // we don't want to drop an element from the buffer if we then 
    // can't write it! so we peek at the buffer, try to write, and pop 
    // if successful 
    value=buffer[0] + 32768// tsf produces *signed* shorts, i2s wants *unsigned* 
    i2s_write(i2sPort, &value, sizeof(value), &written, 0);
    if (written > 0) buffer.pop(value);
  }
 
  // fill the ringbuffer from the renderer 
  int toFill = buffer.maxSize() - buffer.size();
  if (toFill > 100{
    // our buffer tells us how much space it has, but it can't give us 
    // a contiguous segment of RAM to write into; we need an 
    // intermediate buffer 
    tsf_render_short(g_TinySoundFont, intermediateBuffer, toFill, 0);
    for (int i=0;i<toFill;++i) {
      buffer.push(intermediateBuffer[i]);
    }
  }
 
  /* I'm sure there's a better / smarter / faster way of doing that
     data transfer, but this works and I understand it
   */
 
  // did our work take less than 1 millisecond? wait a bit more 
  while (millis() == t) {
    delayMicroseconds(100);
  }
 
  // now we can read the sensor 
  if (sense(currentSensor)) { // currently pressed 
    if (!pressed[currentSensor]) { // was not pressed previously? 
      Serial.print(currentSensor);
      Serial.println(" pressed!");
 
      pressed[currentSensor]=1// mark it pressed 
      if (g_TinySoundFont) {
        // start the note 
        tsf_note_on(g_TinySoundFont, 036 + currentSensor, 1.0f);
      }
    }
  }
  // not currently pressed 
  else if (pressed[currentSensor]) { // was pressed previously? 
    Serial.print(currentSensor);
    Serial.println(" released!");
 
    pressed[currentSensor]=0// mark it no longer pressed 
    if (g_TinySoundFont) {
      // stop the note 
      tsf_note_off(g_TinySoundFont, 036 + currentSensor);
    }
  }
 
  // go to the next sensor 
  currentSensor = (currentSensor+1)%(row_count*col_count);
}