Polyphonic Piano Accordion made from a cheap Melodica, some buttons and an ESP32 microcontroller
To make the experience fit your profile, pick a username and tell us what interests you.
We found and based on your interests.
icp103450s.pdfSamsung ICP103450S Lithium Ion Polymer Battery Cell ManualAdobe Portable Document Format - 408.76 kB - 09/17/2024 at 02:35 |
|
|
I would like to have batteries inside my Arduino Accordion eventually. As I am still waiting for some 10K resistors to arrive from China, I have decided to see if I could find some batteries around.
I opened an old (and broken) laptop to see what type of battery it had inside. This is the one:
That is the best image I got from the Internet and the specs can be read.
Then I proceeded to open it (be careful to not puncture the Lithium Polymer cells! They can explode!). I have cut one thumb and an index finger in the process, be careful...
These are Samsung batteries model ICP103450S. I managed to find a nice manual for it.
Each one could hold 2000 mAh (when new, I don't know if these are dead or not).
Multimitre time and, as expected, they are sort of dead (they read between 1.7 and 1.8 volts). So I found a very interesting article describing how to "revive" these https://www.instructables.com/Recovering-Lithium-Ion-Batteries/.
It turns out that I don't have that fancy battery charger. What I have instead is this little module here:
So, according to this video, I can reduce the charging current from 1A to 130mA using a 10k resistor. This other video shows how to replace the resistor. Since this battery supports (when new) a charging current of 1700mA, this should be enough to charge it very very slowly and prevent an explosion (I hope!).
So, here is the board with the new 10k resistor:
And here is the video of the charging process (these clamp multimeters don't work so well with low currents, but we can see it is less than 1A now):
Now I am going to wait and monitor to see how it goes.
TBC...
As I mentioned in the previous log, I am going to use an NC (normally closed) circuit for the treble keyboard of the accordion.
The idea is to have something like this:
I kept the other wires out of this diagram for the sake of simplicity (the 74HC165 shift register needs to be connected to the ESP32 too).
So, when the key is idle, the circuit is CLOSED, hence NC. When you press a key, that circuit is OPEN.
To test this I found a very ingenious approach based on https://www.instructables.com/simple-normally-closed-switch/. It uses wooden pegs as NC switches, amazing! Right?
Piece of advice: those drawing pins are sharp and can hurt you, that is why I have inverted their position in the wooden peg.
For the copper strip, I am using the melodica reeds, since it is made of metal and was in hand. As I am not a good solderer I decided to connect the keys' resistors mechanically only (a.k.a. twisting the wires together).
And here is the result (I found it amazing :D)
Next step: make the real keyboard!
This is my second time writing this, after I uploaded the pictures, Hackaday crashed and I need to write everything again...
Long-story-short: I bought this cheap plastic melodica to be the treble keyboard of my Arduino Accordion. Why? It is cheap, has 32 keys, starts at note F (like most of the 41-key real accordions do) and it is cheap (again).
I disassembled it and here are some pictures:
I am genuinely surprised with the build quality, it is sturdy, and it is good! It is not like those kids' keyboards people transform into MIDI controllers, this has individual keys, felts to damp the keys and prevent unwanted keystroke sounds and a very interesting mechanism that could allow me to implement a velocity-sensitive keyboard, although I won't do it on this project, not now at least.
This is the key mechanism in action:
As you can see, two contact points can be used as a switch: the back of the key, where there is a valve that opens when you press it and right below the key tip, which requires you to press the key completely to create a contact. That is why I say you can make it velocity-sensitive, you can have both contacts per key and measure the time between them to calculate the note velocity. But that would require 64 inputs (2 per key) and I am good for now.
I will only use the valve as a normally closed switch, where the key is the negative (ground) and a copper conductive strip is the positive (VCC). It will make sense when I show my proof-of-concept working!
Next step: proof-of-concept for the NC (normally-closed) switches.
After trying many different libraries I finally settled on https://github.com/shorepine/amy
It just works, it is simple to use, and has a great variety of patches (I am using the Yamaha DX7 Accordion in the video) and is event-driven, so it is a breeze to develop for.
Here is the repo https://github.com/campidelli/arduino-accordion, you can check the other branches where I have tested:
They all have pros and cons, but I found Amy the easiest to work with.
Here is the video with the accordion sound!
Next step: the 74HC165 chips got here, I will add them to the circuit and start looking at the melodica keyboard.
Quick one here, now it is polyphonic!
Here is the sound (I am trying to replicate this recipe from Floyd Steinberg, not there yet).
And the sketch (that only works like this, when I try to separate it in classes it doesn't perform well):
#include "MozziConfigValues.h"
#define MOZZI_AUDIO_MODE MOZZI_OUTPUT_I2S_DAC
#define MOZZI_I2S_PIN_BCK 26
#define MOZZI_I2S_PIN_WS 25
#define MOZZI_I2S_PIN_DATA 22
#define MOZZI_CONTROL_RATE 2048
#include <Arduino.h>
#include <WiFi.h>
#include <Mozzi.h>
#include <Oscil.h>
#include <ADSR.h>
#include <mozzi_midi.h>
#include <tables/saw2048_int8.h>
#include <tables/square_no_alias_2048_int8.h>
#include "Esp32SynchronizationContext.h"
#include "Keyboard.h"
#define LED_PIN 2
#define MAX_VOICES 10
#define SAMPLE_RATE SAW2048_NUM_CELLS
#define VOLUME 0.95f
// Envelope parameters
unsigned int attackTime = 50;
unsigned int decayTime = 200;
unsigned int sustainDuration = 8000;
unsigned int releaseTime = 200;
byte attackLevel = 96;
byte decayLevel = 64;
// Voice structure
struct Voice {
Oscil<SAMPLE_RATE, AUDIO_RATE> osc1;
Oscil<SAMPLE_RATE, AUDIO_RATE> osc2;
ADSR<CONTROL_RATE, AUDIO_RATE> envelope;
byte note;
long triggeredAt;
};
Voice voices[MAX_VOICES];
// Thread-safe synchronization context
Esp32SynchronizationContext syncContext;
bool updateRequested = false;
Keyboard keyboard;
// Finds a free voice. It can be either a voice not in use
// or the oldest one if all of them are being used
int getFreeVoice() {
int voiceIndex = -1;
long oldestTriggeredAt = millis();
for (int i = 0; i < MAX_VOICES; i++) {
if (!voices[i].envelope.playing()) {
return i;
} else if (voices[i].triggeredAt < oldestTriggeredAt) {
oldestTriggeredAt = voices[i].triggeredAt;
voiceIndex = i;
}
}
return voiceIndex;
}
void noteOn(byte note) {
for (int i = 0; i < MAX_VOICES; i++) {
if (voices[i].envelope.playing() && voices[i].note == note) {
// This note is already being played, ignore
return;
}
}
int freeVoice = getFreeVoice();
float frequency = mtof(float(note));
voices[freeVoice].osc1.setFreq(frequency);
float detuneFactor = pow(2.0, 10.0 / 1200.0);
voices[freeVoice].osc2.setFreq(frequency * detuneFactor * 2); // 10 cents detuned + 1 octave up
voices[freeVoice].envelope.noteOn();
voices[freeVoice].note = note;
voices[freeVoice].triggeredAt = millis();
digitalWrite(LED_PIN, HIGH);
}
void noteOff(byte note) {
int activeNotes = 0;
for (int i = 0; i < MAX_VOICES; i++) {
if (note == voices[i].note) {
voices[i].note = 0;
voices[i].envelope.noteOff();
}
activeNotes += voices[i].note;
}
if (activeNotes == 0) {
digitalWrite(LED_PIN, LOW);
}
}
// Callback functions for handling key press and release events
void onKeyPress(int key) {
byte note = key + 60; // key 0 == C4 == 60
noteOn(note);
}
void onKeyRelease(int key) {
byte note = key + 60;
noteOff(note);
}
void updateControl() {
if (!syncContext.update()) {
Serial.println("Could not update synchronization context");
}
if (updateRequested) {
keyboard.update();
updateRequested = false;
}
// Update the envelopes
for (int i = 0; i < MAX_VOICES; i++) {
voices[i].envelope.update();
}
}
AudioOutput updateAudio() {
long outputSample = 0;
// Accumulate sample values from all playing voices
for (int i = 0; i < MAX_VOICES; i++) {
if (voices[i].envelope.playing()) {
outputSample += (voices[i].osc1.next() + voices[i].osc2.next()) * voices[i].envelope.next();
}
}
outputSample *= VOLUME;
return MonoOutput::fromNBit(24, outputSample);
}
void updateKeyboardTask(void *state) {
// RUNS ON OTHER CORE
while (true) {
if (updateRequested) {
delay(1); // Feed watchdog
continue; // Don't do anything if the main thread is still processing the last update
}
// Request the main thread to update keyboard states
syncContext.send(
[](void *state) {
// RUNS ON MAIN CORE
updateRequested = true;
}
);
delay(10); // Feed watchdog
}
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_OFF); // Disable WiFi to conserve...
Read more »
To use fewer ESP32 pins, I need to multiplex the inputs as I have been doing already (using a matrix scan) but I can go further and add shift registers to save more pins!
However, the subject can be quite confusing and intimidating, at least to me. I thought I could use a single 74HC595 to scan both the COLUMNS and ROWS of my matrix, but it turns out I can't. That is because the 74HC595 is a serial IN and parallel OUT shift register. To scan a matrix I need to write to the columns (or rows, pick one) and read the rows (or columns if you have picked the rows before).
I bought a very nice module that has three 74HC595 chained, allowing you to expand your ports to a whopping 24 outputs! But I still need a way to read several inputs, and for that, there is another shift register called 74HC165.
Since I am still waiting for my 74HC165 to arrive from China, I decided to use a small 74HC595 I had here to test the matrix scanning, using it to output signal to 4 columns and read the 2 rows using the ESP32 input pins.
Well, it didn't work. So I had to check if my shift register was working and for that, I had to learn more about it. So I checked this tutorial here (https://dronebotworkshop.com/shift-registers/) and it is amazing. Here is the Hello World example:
#include <Arduino.h>
// ST_CP pin 22
const int latchPin = 22;
// SH_CP pin 21
const int clockPin = 21;
// DS pin 23
const int dataPin = 23;
void setup () {
// Setup pins as Outputs
pinMode(latchPin, OUTPUT);
pinMode(clockPin, OUTPUT);
pinMode(dataPin, OUTPUT);
}
void loop() {
// Count from 0 to 255 and display in binary
for (int numberToDisplay = 0; numberToDisplay < 256; numberToDisplay++) {
// ST_CP LOW to keep LEDs from changing while reading serial data
digitalWrite(latchPin, LOW);
// Shift out the bits
shiftOut(dataPin, clockPin, MSBFIRST, numberToDisplay);
// ST_CP HIGH change LEDs
digitalWrite(latchPin, HIGH);
delay(500);
}
}
And the demo!
Now that I now that my shift register IS working, I had to recheck the wiring and my program, it turns out that I had misread the data pin GPIO number and set it to 27 instead of 23. Now it is working! And it supports multi-pressed buttons! Here is the sketch:
#include <Arduino.h>
// Pin definitions
const int DATA_PIN = 23; // Data (SER) -> GPIO 23
const int CLOCK_PIN = 21; // Clock (SRCLK) -> GPIO 21
const int LATCH_PIN = 22; // Latch (RCLK) -> GPIO 22
// Matrix dimensions
const int COLS = 4;
const int ROWS = 2;
// Row pins
const int rowPins[ROWS] = {12, 14}; // Row 1 -> GPIO 12, Row 2 -> GPIO 14
// Column to 74HC595 mapping
const int colPins[COLS] = {0, 1, 2, 3}; // Q0 to Q3 on the 74HC595
// Array to keep track of button states (0 = not pressed, 1 = pressed)
bool buttonStates[ROWS][COLS] = {false};
// Set a specific column LOW by controlling the 74HC595
void setColumn(int col) {
uint8_t colValue = ~(1 << col); // Set only the selected column LOW
shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, colValue);
// Latch to transfer the data to output pins
digitalWrite(LATCH_PIN, LOW);
digitalWrite(LATCH_PIN, HIGH);
// Small delay to allow the shift register output to stabilize
delayMicroseconds(10);
}
void onButtonPress(int button) {
Serial.print("Button ");
Serial.print(button);
Serial.println(" pressed");
}
void onButtonRelease(int button) {
Serial.print("Button ");
Serial.print(button);
Serial.println(" released");
}
void setup() {
// Initialize the Serial
Serial.begin(115200);
// Set up the row pins as inputs with pull-up resistors
for (int i = 0; i < ROWS; i++) {
pinMode(rowPins[i], INPUT_PULLUP);
}
// Set up the control pins for the 74HC595
pinMode(DATA_PIN, OUTPUT);
pinMode(CLOCK_PIN, OUTPUT);
pinMode(LATCH_PIN, OUTPUT);
}
void loop() {
for (int col = 0; col < COLS; col++) {
// Set the current column to LOW
setColumn(col);
// Check each row for a key press
for (int row = 0; row < ROWS; row++) {
...
Read more »
Hey, we have some sound being triggered now!
Only four notes (C, D, E and F) sampling at the octave 4.
Is it Polyphonic? Hum, sort of? I mean, it plays the four notes at the same time. Is it pleasant to hear? Nope, not at all (yet!).
Here are all the pieces together:
And the sketch that is producing this wonderful sound haha:
#include "MozziConfigValues.h"
#define MOZZI_AUDIO_MODE MOZZI_OUTPUT_I2S_DAC
#define MOZZI_I2S_PIN_BCK 26
#define MOZZI_I2S_PIN_WS 25
#define MOZZI_I2S_PIN_DATA 22
#define MOZZI_CONTROL_RATE 256
#include <Arduino.h>
#include <Keypad.h>
#include <Mozzi.h>
#include <Oscil.h>
#include <tables/cos8192_int8.h>
#include <mozzi_midi.h>
#include <ADSR.h>
// Envelope controllers
#define ATTACK 100
#define DECAY 200
#define SUSTAIN 500 // Reduced sustain time
#define RELEASE 500 // Reduced release time
#define ATTACK_LEVEL 127 // Lower maximum amplitude level
#define DECAY_LEVEL 127 // Lower decay level
// SETTINGS
#define OCTAVE 4
#define MAX_POLYPHONY LIST_MAX
// Keypad configuration
const byte ROWS = 2;
const byte COLS = 2;
byte key_indexes[ROWS][COLS] = {
{1, 2},
{3, 4}
};
byte rowPins[ROWS] = {14, 12};
byte colPins[COLS] = {32, 33};
Keypad keypad = Keypad(makeKeymap(key_indexes), rowPins, colPins, ROWS, COLS);
// Voices
struct Voice {
Oscil<COS8192_NUM_CELLS, AUDIO_RATE> osc;
ADSR<MOZZI_CONTROL_RATE, AUDIO_RATE> env;
byte note;
};
Voice voices[MAX_POLYPHONY];
void noteOff(byte note) {
for (int i = 0; i < MAX_POLYPHONY; i++) {
if (voices[i].note == note) {
voices[i].env.noteOff();
voices[i].note = 0;
Serial.print("Note Off: ");
Serial.println(note);
return;
}
}
}
void noteOn(byte note) {
int noteIndex = 0;
for (; noteIndex < MAX_POLYPHONY; noteIndex++) {
if (!voices[noteIndex].env.playing()) {
// This voice is not playing, let's use it then
break;
}
if (voices[noteIndex].note == note) {
// This note is already playing, ignore
return;
}
if (noteIndex + 1 == MAX_POLYPHONY) {
// This is the last voice and it is occupied,
// let's steal the oldest voice (index 0) and reuse it
noteIndex = 0;
break;
}
}
voices[noteIndex].note = note;
voices[noteIndex].osc.setFreq(mtof(note));
voices[noteIndex].env.noteOn();
Serial.print("Note On: ");
Serial.println(note);
}
void play() {
if (keypad.getKeys()) {
for (int i = 0; i < LIST_MAX; i++) {
if (keypad.key[i].stateChanged) {
byte note = (OCTAVE * 12) + 11 + keypad.key[i].kchar;
KeyState state = keypad.key[i].kstate;
if (state == PRESSED) {
noteOn(note);
} else if (state == RELEASED) {
noteOff(note);
}
}
}
}
}
void setup() {
Serial.begin(115200);
for (int i = 0; i < MAX_POLYPHONY; i++) {
voices[i].osc.setTable(COS8192_DATA);
voices[i].env.setADLevels(ATTACK_LEVEL, DECAY_LEVEL);
voices[i].env.setTimes(ATTACK, DECAY, SUSTAIN, RELEASE);
}
startMozzi(MOZZI_CONTROL_RATE);
}
void updateControl() {
play();
for (int i = 0; i < MAX_POLYPHONY; i++) {
voices[i].env.update();
}
}
AudioOutput updateAudio() {
long currentSample = 0;
for (unsigned int i = 0; i < MAX_POLYPHONY; i++) {
if (voices[i].env.playing()) {
currentSample += voices[i].osc.next() * voices[i].env.next();
}
}
return MonoOutput::fromAlmostNBit(20, currentSample);
}
void loop() {
audioHook();
}
Next step: more buttons!
Yesterday I managed to create a simple program to listen to a web radio (from Brazil, of course :D )
But, before that could happen, I had to put my non-existent soldering skills to the test! Yes, my PCM5102 board didn't have the jumpers soldered, I had to solder them myself and, of course, the header pins.
Anyway, here is the "radio" put together and its code!
I have followed this article https://www.xtronical.com/i2sinternetradio/ that uses this library https://github.com/schreibfaul1/ESP32-audioI2S but, note the PINs I had to use, they are different from the article, I guess that my ESP32 and my PCM5102 are slightly different.
#include <Arduino.h>
#include <WiFi.h>
#include <Audio.h>
#define I2S_PIN_BCLK (26) // Bit Clock (BCK)
#define I2S_PIN_WS (25) // Word Select (L/R)
#define I2S_PIN_DOUT (22) // Data Out (DOUT)
const char* ssid = "your-wifi-network";
const char* password = "***********";
const char* radioURL = "https://www.appradio.app:8010/live?1724128347016";
Audio audio;
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.println("Connecting to WiFi...");
}
Serial.println("Connected to WiFi");
audio.setConnectionTimeout(5000, 5000);
audio.setPinout(I2S_PIN_BCLK, I2S_PIN_WS, I2S_PIN_DOUT);
audio.setVolume(5); // 0...21
audio.connecttohost(radioURL);
}
void loop() {
audio.loop();
}
With that setup, I could hear 105.1 FM from Sao Paulo, using some headphones.
But there is more! Now it is time for the PAM8403 and speakers! Here is a demonstration:
Next step: Put the buttons back and make it sound with 4 polyphonic notes!
Okay, after a lot of coding with my buddy Chat GPT, I made some progress. You can check the code here https://github.com/campidelli/arduino-accordion
I am using OOP because, as a Java developer, it is much easier to understand what is happening.
Here is the project structure so far:
This could be used for any virtual instrument, in this case, I am making an Accordion, but it could be a Marimba, Drums, Piano, etc.
The instrument gets one or more Inputs (I am using one named PianoKeyboard) and one or more outputs (I am printing the Notes/Chords to the terminal).
An output example here:
Because I am using the Keypad library, I can simultaneously press up to 10 keys (https://github.com/Chris--A/Keypad/blob/master/examples/MultiKey/MultiKey.ino)
The next stop is to generate some actual sounds!
If everything goes right I intend to have a very portable electronic piano accordion with 32 treble keys and 72 (!!!!) Stradella bass keys. The ESP32 board I am using has 32 free pins, potentially less, as there are always some limitations for these ports.
So, I need a matrix (or 2) to multiplex those buttons and use fewer pins.
Therefore, I can have a matrix of 4x8 for the treble keyboard (32 keys) and another matrix of 8x9 for the Stradella bass (72) buttons.
4 + 8 + 8 + 9 = 29 pins! Maybe it is feasible, maybe it is not and I will need an actual multiplexer or use 2 ESP32 boards? Who knows? I will eventually find that out.
It would be something like this, from the brilliant Rafael Corvino, a fellow Brazilian like me (https://www.instagram.com/corvino_acordeonmidi).
But it is not a MIDI accordion, it would not need a computer to generate the sounds.
Anyway, I don't have the Melodica yet (yes, it will be my keyboard) nor the buttons for the bass, I will start small, a 2x2 matrix with diodes to prevent ghosting, this video explains it brilliantly:
This is my little prototype, it looks silly but it was hard for me to get there, I made this mistake https://www.reddit.com/r/ElectronicsRepair/comments/103pfk8/im_building_a_keyboard_matrix_with_a_micro/ but now it is okay!
I only have 4 buttons for now (still waiting for the Temu order to get here :)
This very simple program shows the buttons states, you can press them all at once, neat!
#include <Arduino.h>
#include <Keypad.h>
const byte ROWS = 2;
const byte COLS = 2;
char keys[ROWS][COLS] = {
{'1','2'},
{'3','4'}
};
byte rowPins[ROWS] = {22, 23};
byte colPins[COLS] = {25, 26};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
unsigned long loopCount;
unsigned long startTime;
String msg;
void setup() {
Serial.begin(9600);
loopCount = 0;
startTime = millis();
msg = "";
}
void loop() {
loopCount++;
if ((millis() - startTime) > 5000) {
Serial.print("Average loops per second = ");
Serial.println(loopCount/5);
startTime = millis();
loopCount = 0;
}
// Fills keypad.key[] array with up-to 10 active keys.
// Returns true if there are ANY active keys.
if (keypad.getKeys()) {
for (int i = 0; i < LIST_MAX; i++) { // Scan the whole key list.
if (keypad.key[i].stateChanged) { // Only find keys that have changed state.
switch (keypad.key[i].kstate) { // Report active key state : IDLE, PRESSED, HOLD, or RELEASED
case PRESSED:
msg = " PRESSED.";
break;
case HOLD:
msg = " HOLD.";
break;
case RELEASED:
msg = " RELEASED.";
break;
case IDLE:
msg = " IDLE.";
}
Serial.print("Key ");
Serial.print(keypad.key[i].kchar);
Serial.println(msg);
}
}
}
} // End loop
Create an account to leave a comment. Already have an account? Log In.
Become a member to follow this project and never miss any updates