MPFII: a Touch-Sensitive Synthesizer

Introduction

Welcome to this complete tutorial for building the MPFII (Micro Polyphonic Flexible Instrument II), a modern homage to Taiwanese Apple II clones. This touch-sensitive synthesizer combines the power of the ESP32 with an intuitive interface based on a resistive wire to create a unique and expressive musical instrument.

The MPFII allows you to play notes by simply moving your finger along a resistive wire, while offering advanced features like multiple waveforms, delay effects, arpeggios, and different musical scales.

Required Materials

Main Components

  • ESP32-WROVER (with built-in DAC) - ~$10
  • 128x64 OLED Display (I2C, SSD1306) - ~$5
  • Nichrome Resistive Wire (100Ω, 1m) - ~$3
  • LM386 Audio Amplifier - ~$2
  • 8Ω 0.5W Speaker - ~$3
  • 4 Push Buttons - ~$2
  • Resistors: 10kΩ (x4), 1kΩ (x1) - ~$1
  • Breadboard or custom PCB - ~$5
  • Enclosure (3D printed or existing box) - ~$10

Estimated total cost: ~$40

Tools

  • Soldering iron
  • Wiring wire
  • Multimeter
  • Computer with Arduino IDE

Wiring Diagram

Circuit Overview

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ESP32               Components
----- ----------
GPIO25 (DAC1) -----> R1 (1kΩ) -----> LM386 Audio Input
GND ----------------- GND
GPIO34 <------------ Resistive Wire (100Ω)
3.3V ---- R2 (10kΩ) -----> Resistive Wire
GND ----------------- Other end of resistive wire

GPIO21 -----> SDA (OLED)
GPIO22 -----> SCL (OLED)
3.3V -----> VCC (OLED)
GND -----> GND (OLED)

GPIO13 -----> Waveform Button
GPIO12 -----> Delay Button
GPIO14 -----> Arpeggio Button
GPIO27 -----> Scale Button

LM386 Amplifier Detailed Wiring

1
2
3
4
5
6
7
8
LM386:
Pin 1 (Gain) -----> 10µF Capacitor -----> Pin 8
Pin 2 (In-) -----> GND
Pin 3 (In+) -----> from R1 (1kΩ)
Pin 4 (GND) -----> GND
Pin 5 (Vout) -----> 220µF Capacitor -----> Speaker
Pin 6 (VCC) -----> 5V
Pin 7 (Bypass) -----> 100µF Capacitor -----> GND

Physical Assembly

Step 1: Preparing the Resistive Wire

The resistive wire is the central element of our touch interface. For optimal response:

  1. Cut 30cm of 100Ω nichrome wire
  2. Stretch it slightly to make it perfectly straight
  3. Solder the ends to terminal blocks or directly to the board
  4. Calibrate the voltage: one end to 3.3V, the other to GND

Pro tip: For better durability, you can wrap the resistive wire with enameled copper wire at the solder points.

Step 2: Button Mounting

The 4 push buttons control different functions:

  1. Waveform (GPIO13): Cycle through 5 waveforms
  2. Delay (GPIO12): Enable/disable delay with 5 different times
  3. Arpeggio (GPIO14): Enable/disable arpeggio mode
  4. Scale (GPIO27): Change the musical scale

Each button should be wired with a 10kΩ pull-down resistor.

Step 3: OLED Display Installation

The OLED display shows all parameters in real-time:

  1. Connect SDA to GPIO21
  2. Connect SCL to GPIO22
  3. Power with 3.3V and GND
  4. Mount it in a visible location on your enclosure

Step 4: Audio Wiring

The audio chain is crucial for sound quality:

  1. GPIO25 (DAC1) → 1kΩ Resistor → LM386 Input
  2. LM386 Output → 220µF Capacitor → Speaker
  3. Add a 10kΩ potentiometer at input for volume control (optional)

ESP32 Programming

Arduino IDE Configuration

  1. Install ESP32 support:

    • File > Preferences
    • Additional Boards Manager URLs: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    • Tools > Board Manager > Search for “ESP32” and install
  2. Install required libraries:

    • Adafruit GFX Library
    • Adafruit SSD1306

The Complete Code

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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
#include <driver/dac.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// OLED Display Configuration
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Pin Configuration
#define TOUCH_PIN 34 // GPIO for reading resistive wire
#define DAC_PIN DAC1 // GPIO25 for audio output
#define WAVEFORM_BTN 13 // Waveform button
#define DELAY_BTN 12 // Delay button
#define ARPEGGIO_BTN 14 // Arpeggio button
#define SCALE_BTN 27 // Scale button
#define SAMPLE_RATE 44100 // Sampling rate

// Different musical scales
const float majorScale[] = {261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25}; // C major
const float minorScale[] = {261.63, 293.66, 311.13, 349.23, 392.00, 415.30, 466.16, 523.25}; // C minor
const float orientalScale[] = {261.63, 277.18, 311.13, 349.23, 392.00, 415.30, 466.16, 523.25}; // Oriental
const float chineseScale[] = {261.63, 293.66, 329.63, 392.00, 440.00, 523.25, 587.33, 659.25}; // Chinese pentatonic

// Pointers to scales
const float* scales[] = {majorScale, minorScale, orientalScale, chineseScale};
const char* scaleNames[] = {"Major", "Minor", "Oriental", "Chinese"};
const int scaleSizes[] = {8, 8, 8, 8};

// Global variables
int currentScale = 0;
int currentNote = 0;
float frequency = 261.63;
float phase = 0.0;
unsigned long lastTime = 0;

// Waveforms
enum Waveform { SINE, SQUARE, SAW, TRIANGLE, NOISE };
Waveform currentWaveform = SINE;
const char* waveformNames[] = {"Sine", "Square", "Saw", "Triangle", "Noise"};

// Delay
bool delayEnabled = false;
int currentDelay = 0;
const int delayTimes[] = {100, 200, 300, 500, 800}; // in ms
const char* delayNames[] = {"100ms", "200ms", "300ms", "500ms", "800ms"};
#define DELAY_BUFFER_SIZE 44100 // 1 second at 44.1kHz
int delayBuffer[DELAY_BUFFER_SIZE];
int delayIndex = 0;

// Arpeggio
bool arpeggioEnabled = false;
int currentArpeggio = 0;
unsigned long arpeggioTime = 0;
int arpeggioNote = 0;
const int arpeggioSpeeds[] = {100, 150, 200, 300, 400}; // in ms
const char* arpeggioNames[] = {"Fast", "Normal", "Slow", "V Slow", "Ultra Slow"};

// Button management
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;

void setup() {
Serial.begin(115200);

// Initialize OLED display
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.println("MPFII Ready!");
display.display();
delay(1000);

// Initialize DAC
dac_output_enable(DAC_CHANNEL_1);

// Configure pins
pinMode(TOUCH_PIN, INPUT);
pinMode(WAVEFORM_BTN, INPUT_PULLDOWN);
pinMode(DELAY_BTN, INPUT_PULLDOWN);
pinMode(ARPEGGIO_BTN, INPUT_PULLDOWN);
pinMode(SCALE_BTN, INPUT_PULLDOWN);

// Initialize delay buffer
for(int i=0; i<DELAY_BUFFER_SIZE; i++) {
delayBuffer[i] = 128;
}

updateDisplay();
Serial.println("MPFII Synthesizer ready!");
}

void loop() {
// Handle buttons
handleButtons();

// Read position on resistive wire
int touchValue = analogRead(TOUCH_PIN);

// Map value to note
int newNote = map(touchValue, 0, 4095, 0, scaleSizes[currentScale]-1);
newNote = constrain(newNote, 0, scaleSizes[currentScale]-1);

if (newNote != currentNote && !arpeggioEnabled) {
currentNote = newNote;
frequency = scales[currentScale][currentNote];
}

// Generate audio
generateAudio();
}

void handleButtons() {
// Waveform button
if(digitalRead(WAVEFORM_BTN) == HIGH) {
if(millis() - lastDebounceTime > debounceDelay) {
currentWaveform = (Waveform)((currentWaveform + 1) % 5);
updateDisplay();
}
lastDebounceTime = millis();
}

// Delay button
if(digitalRead(DELAY_BTN) == HIGH) {
if(millis() - lastDebounceTime > debounceDelay) {
delayEnabled = !delayEnabled;
if(delayEnabled) {
currentDelay = (currentDelay + 1) % 5;
}
updateDisplay();
}
lastDebounceTime = millis();
}

// Arpeggio button
if(digitalRead(ARPEGGIO_BTN) == HIGH) {
if(millis() - lastDebounceTime > debounceDelay) {
arpeggioEnabled = !arpeggioEnabled;
if(arpeggioEnabled) {
currentArpeggio = (currentArpeggio + 1) % 5;
arpeggioTime = millis();
arpeggioNote = 0;
}
updateDisplay();
}
lastDebounceTime = millis();
}

// Scale button
if(digitalRead(SCALE_BTN) == HIGH) {
if(millis() - lastDebounceTime > debounceDelay) {
currentScale = (currentScale + 1) % 4;
currentNote = 0;
frequency = scales[currentScale][currentNote];
updateDisplay();
}
lastDebounceTime = millis();
}
}

void generateAudio() {
unsigned long currentTime = micros();
float deltaTime = (currentTime - lastTime) / 1000000.0;
lastTime = currentTime;

// Handle arpeggio
if(arpeggioEnabled) {
if(millis() - arpeggioTime > arpeggioSpeeds[currentArpeggio]) {
arpeggioNote = (arpeggioNote + 1) % scaleSizes[currentScale];
frequency = scales[currentScale][arpeggioNote];
arpeggioTime = millis();
}
}

// Calculate phase
phase += 2.0 * PI * frequency * deltaTime;
if (phase > 2.0 * PI) {
phase -= 2.0 * PI;
}

// Generate value based on waveform
int audioValue = 128; // Default value (middle)

switch(currentWaveform) {
case SINE:
audioValue = (int)((sin(phase) + 1.0) * 127.5);
break;
case SQUARE:
audioValue = (sin(phase) > 0) ? 255 : 0;
break;
case SAW:
audioValue = (int)((phase / (2.0 * PI)) * 255);
break;
case TRIANGLE:
audioValue = (int)((fabs(fmod(phase, 2.0*PI) - PI) / PI) * 255);
break;
case NOISE:
audioValue = random(0, 255);
break;
}

audioValue = constrain(audioValue, 0, 255);

// Apply delay
if(delayEnabled) {
int delaySamples = (delayTimes[currentDelay] * SAMPLE_RATE) / 1000;
int delayedValue = delayBuffer[(delayIndex - delaySamples + DELAY_BUFFER_SIZE) % DELAY_BUFFER_SIZE];
audioValue = (audioValue + delayedValue) / 2; // Mix wet/dry
}

// Store in delay buffer
delayBuffer[delayIndex] = audioValue;
delayIndex = (delayIndex + 1) % DELAY_BUFFER_SIZE;

// Send to DAC
dac_output_voltage(DAC_CHANNEL_1, audioValue);

delayMicroseconds(22); // ~44.1kHz
}

void updateDisplay() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);

display.println("MPFII Synth");
display.println("-----------");

display.print("Scale: ");
display.println(scaleNames[currentScale]);

display.print("Wave: ");
display.println(waveformNames[currentWaveform]);

display.print("Delay: ");
if(delayEnabled) {
display.println(delayNames[currentDelay]);
} else {
display.println("OFF");
}

display.print("Arp: ");
if(arpeggioEnabled) {
display.println(arpeggioNames[currentArpeggio]);
} else {
display.println("OFF");
}

display.print("Note: ");
display.print(currentNote);
display.print(" (");
display.print(frequency, 1);
display.println("Hz)");

display.display();
}

Detailed Features

1. Touch Interface

The heart of the MPFII is its touch interface based on a resistive wire:

  • Principle: The resistive wire acts as a linear potentiometer
  • Reading: The ESP32 measures voltage via its ADC (GPIO34)
  • Mapping: The 0-4095 value is mapped to notes of the selected scale
  • Precision: 8 notes per scale, evenly distributed along the wire length

2. Waveforms

The MPFII offers 5 distinct waveforms:

  1. Sine: Pure, soft wave, ideal for pads
  2. Square: Typical 8-bit sound, rich in harmonics
  3. Saw: Bright sound, perfect for leads
  4. Triangle: Soft, synthetic sound, between sine and square
  5. Noise: White noise, useful for percussion and effects

3. Musical Scales

Four scales are available to explore different musical styles:

  • Major: Standard Western scale, happy
  • Minor: Melancholic, dramatic scale
  • Oriental: Characteristic Middle Eastern intervals
  • Chinese: Pentatonic, traditional Asian sound

4. Delay Effect

The delay adds depth to the sound:

  • 5 different times: 100ms, 200ms, 300ms, 500ms, 800ms
  • Circular buffer: 44100 samples (1 second at 44.1kHz)
  • Wet/dry mix: 50% original signal, 50% delayed signal

5. Arpeggiator

The arpeggiator automates note playing:

  • 5 speeds: From fast to ultra slow
  • Automatic cycling: Plays through all scale notes
  • Timing: Based on millis() for temporal accuracy

Calibration and Settings

Resistive Wire Calibration

For optimal response, calibrate your wire:

  1. Open the serial monitor (115200 baud)
  2. Touch the wire ends and note the min/max values
  3. Adjust the mapping in the code if necessary:
    1
    int newNote = map(touchValue, minVal, maxVal, 0, scaleSizes[currentScale]-1);

Volume Control

To add volume control:

  1. Add a 10kΩ potentiometer on GPIO35
  2. Modify the code:
    1
    2
    3
    4
    5
    #define VOLUME_PIN 35
    int volume = analogRead(VOLUME_PIN) / 16; // 0-255

    // In generateAudio():
    audioValue = (audioValue * volume) / 255;

Latency Optimization

For faster response:

  1. Reduce SAMPLE_RATE to 22050 for less latency
  2. Optimize delayMicroseconds() according to your setup
  3. Disable Serial.begin() after debugging

Enclosure and Design

Option 1: 3D Printed Enclosure

Create a custom enclosure with these dimensions:

  • External dimensions: 200mm x 100mm x 50mm
  • Display location: 128x64mm with 2mm margin
  • Button locations: 4 holes of 12mm diameter
  • Touch wire area: 250mm x 20mm

Option 2: Reuse

Transform an old keyboard or existing box:

  • Vintage keyboard: Remove keys, keep the case
  • Cigar box: Perfect for a retro look
  • Effect pedal case: Sturdy and professional

Troubleshooting

Common Issues

  1. No sound:

    • Check amplifier wiring
    • Test speaker with a battery
    • Verify DAC is properly initialized
  2. Noise in sound:

    • Add decoupling capacitors (100nF) on power supply
    • Keep audio wires away from power wires
    • Use short, twisted wiring
  3. Display not showing:

    • Check I2C address (usually 0x3C)
    • Test with I2C scanner
    • Check SDA/SCL connections
  4. Inaccurate touch response:

    • Calibrate resistive wire
    • Check pull-up/pull-down resistors
    • Clean the resistive wire

Possible Improvements

  1. Add an LFO:

    1
    2
    3
    4
    float lfoPhase = 0.0;
    float lfoFrequency = 5.0; // 5Hz
    float lfoValue = sin(lfoPhase);
    frequency = baseFrequency * (1.0 + 0.1 * lfoValue); // Vibrato
  2. Low-pass filter:

    1
    float filteredValue = 0.9 * filteredValue + 0.1 * audioValue;
  3. Preset saving:
    Use EEPROM to save settings

  4. MIDI Bluetooth:
    Add Bluetooth module to control external synthesizers

Conclusion

The MPFII is much more than a simple electronics project: it’s a truly innovative musical interface that combines the simplicity of a traditional instrument with the power of digital synthesis. With its modest cost and rich features, it represents an excellent introduction to both electronics and sound synthesis.

I hope I’ve provided you with all the necessary steps to build your own MPFII. Feel free to experiment, modify the code, and customize the design to create an instrument that reflects your personality.

Happy musical creations with your MPFII!