Playing notes over I2S
This commit is contained in:
+47
-113
@@ -1,133 +1,67 @@
|
||||
#include "AudioThread.h"
|
||||
#include "SharedState.h"
|
||||
#include <I2S.h>
|
||||
#include <math.h>
|
||||
|
||||
// --- MIDI ---
|
||||
// Create a MIDI object listening on Serial1 (GP0/GP1)
|
||||
MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI);
|
||||
// I2S Pin definitions
|
||||
// You may need to change these to match your hardware setup (e.g., for a specific DAC).
|
||||
const int I2S_BCLK_PIN = 9; // Bit Clock (GP9)
|
||||
const int I2S_LRC_PIN = 10; // Left-Right Clock (GP10)
|
||||
const int I2S_DOUT_PIN = 11; // Data Out (GP11)
|
||||
|
||||
// --- Forward Declarations ---
|
||||
void fill_audio_buffer(int16_t* buffer, size_t buffer_size);
|
||||
void handleNoteOn(byte channel, byte note, byte velocity);
|
||||
void handleNoteOff(byte channel, byte note, byte velocity);
|
||||
// Audio parameters
|
||||
const int SAMPLE_RATE = 44100;
|
||||
const int16_t AMPLITUDE = 16383; // Use a lower amplitude to avoid clipping (max is 32767 for 16-bit)
|
||||
|
||||
#ifdef TEST_OUT
|
||||
// C Natural Minor Scale notes (C3 to C5) for testing
|
||||
const byte c_minor_scale_notes[] = {
|
||||
48, 50, 51, 53, 55, 56, 58, // C3 octave
|
||||
60, 62, 63, 65, 67, 68, 70, // C4 octave
|
||||
72 // C5
|
||||
// Create an I2S output object
|
||||
I2S i2s(OUTPUT);
|
||||
|
||||
// --- Synthesizer State ---
|
||||
// Frequencies for a C-Major scale to pick from
|
||||
const float NOTE_FREQUENCIES[] = {
|
||||
261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25
|
||||
};
|
||||
#endif
|
||||
const int NUM_NOTES = sizeof(NOTE_FREQUENCIES) / sizeof(NOTE_FREQUENCIES[0]);
|
||||
|
||||
float currentFrequency = 440.0f;
|
||||
double phase = 0.0;
|
||||
unsigned long lastNoteChangeTime = 0;
|
||||
// ---
|
||||
|
||||
void setupAudio() {
|
||||
#ifndef TEST_OUT
|
||||
// --- Initialize MIDI ---
|
||||
// The optocoupler circuit inverts the signal, so we must enable inverse logic.
|
||||
Serial1.setRX(MIDI_RX_PIN);
|
||||
Serial1.setTX(0); // Not using TX
|
||||
Serial1.begin(31250);
|
||||
Serial1.setRXInverse(true);
|
||||
|
||||
MIDI.setHandleNoteOn(handleNoteOn);
|
||||
MIDI.setHandleNoteOff(handleNoteOff);
|
||||
MIDI.begin(MIDI_CHANNEL_OMNI);
|
||||
Serial.println("MIDI Initialized.");
|
||||
#else
|
||||
Serial.println("TEST_OUT mode enabled. Playing random C minor notes.");
|
||||
// Seed random from noise on the volume pot ADC pin
|
||||
randomSeed(analogRead(VOL_POT_PIN));
|
||||
#endif
|
||||
|
||||
// --- Initialize I2S Audio ---
|
||||
// Configure I2S pins
|
||||
i2s.setBCLK(I2S_BCLK_PIN);
|
||||
i2s.setLRCK(I2S_LRCK_PIN);
|
||||
i2s.setDATA(I2S_DATA_PIN);
|
||||
i2s.setDATA(I2S_DOUT_PIN);
|
||||
|
||||
// Set the audio callback function
|
||||
i2s.setBufferCallback(fill_audio_buffer);
|
||||
|
||||
if (!i2s.begin(I2S_STEREO, SAMPLE_RATE, BITS_PER_SAMPLE)) {
|
||||
// Set the sample rate and start I2S communication
|
||||
i2s.setFrequency(SAMPLE_RATE);
|
||||
if (!i2s.begin()) {
|
||||
Serial.println("Failed to initialize I2S!");
|
||||
while (1); // Stop forever
|
||||
while (1); // Halt on error
|
||||
}
|
||||
Serial.println("I2S Initialized.");
|
||||
|
||||
// Seed the random number generator from an unconnected analog pin
|
||||
randomSeed(analogRead(A0));
|
||||
}
|
||||
|
||||
void loopAudio() {
|
||||
#ifdef TEST_OUT
|
||||
static uint32_t last_note_event = 0;
|
||||
static bool is_playing_test_note = false;
|
||||
const uint16_t note_duration = 250; // ms
|
||||
const uint16_t note_gap = 50; // ms
|
||||
unsigned long now = millis();
|
||||
|
||||
// Check if it's time to turn off the current note
|
||||
if (is_playing_test_note && (millis() - last_note_event > note_duration)) {
|
||||
handleNoteOff(1, 0, 0); // Turn note off
|
||||
is_playing_test_note = false;
|
||||
last_note_event = millis(); // Reset timer for the gap
|
||||
// Every 500ms, pick a new random note to play
|
||||
if (now - lastNoteChangeTime > 500) {
|
||||
lastNoteChangeTime = now;
|
||||
int noteIndex = random(0, NUM_NOTES);
|
||||
currentFrequency = NOTE_FREQUENCIES[noteIndex];
|
||||
Serial.println("Playing note: " + String(currentFrequency) + " Hz");
|
||||
}
|
||||
|
||||
// Check if it's time to play a new note
|
||||
if (!is_playing_test_note && (millis() - last_note_event > note_gap)) {
|
||||
// Pick a random note from the scale
|
||||
int note_index = random(sizeof(c_minor_scale_notes) / sizeof(c_minor_scale_notes[0]));
|
||||
byte midi_note = c_minor_scale_notes[note_index];
|
||||
// Generate the sine wave sample
|
||||
double phaseIncrement = 2.0 * M_PI * currentFrequency / SAMPLE_RATE;
|
||||
phase = fmod(phase + phaseIncrement, 2.0 * M_PI);
|
||||
int16_t sample = static_cast<int16_t>(AMPLITUDE * sin(phase));
|
||||
|
||||
// Call NoteOn to set frequency and state
|
||||
handleNoteOn(1, midi_note, 127); // Channel and velocity don't matter here
|
||||
is_playing_test_note = true;
|
||||
last_note_event = millis(); // Reset timer for the duration
|
||||
}
|
||||
#else
|
||||
// Listen for incoming MIDI messages
|
||||
MIDI.read();
|
||||
#endif
|
||||
}
|
||||
|
||||
// --- Audio Generation Callback ---
|
||||
// This function is called by the I2S library on the second core (by default)
|
||||
// to fill the audio buffer. It must be fast and should not do any allocations.
|
||||
void fill_audio_buffer(int16_t* buffer, size_t buffer_size) {
|
||||
if (!g_note_on || g_note_frequency <= 0.0) {
|
||||
// If no note is playing, fill the buffer with silence.
|
||||
memset(buffer, 0, buffer_size * sizeof(int16_t));
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate how much to increment the phase for each sample to get the desired frequency.
|
||||
float phase_increment = (2.0 * PI * g_note_frequency) / SAMPLE_RATE;
|
||||
|
||||
// The maximum amplitude for a 16-bit signed integer.
|
||||
const int16_t max_amplitude = 32767;
|
||||
|
||||
for (size_t i = 0; i < buffer_size; i += 2) { // Process in stereo pairs
|
||||
// Generate a sawtooth wave sample (-1.0 to 1.0)
|
||||
float sample = (g_phase / PI) - 1.0;
|
||||
|
||||
// Increment and wrap the phase
|
||||
g_phase += phase_increment;
|
||||
if (g_phase >= 2.0 * PI) {
|
||||
g_phase -= 2.0 * PI;
|
||||
}
|
||||
|
||||
// Apply volume and scale to 16-bit integer range
|
||||
int16_t final_sample = static_cast<int16_t>(sample * max_amplitude * g_volume);
|
||||
|
||||
// Write the same sample to both left and right channels
|
||||
buffer[i] = final_sample; // Left channel
|
||||
buffer[i + 1] = final_sample; // Right channel
|
||||
}
|
||||
}
|
||||
|
||||
// --- MIDI Callback Functions ---
|
||||
void handleNoteOn(byte channel, byte note, byte velocity) {
|
||||
// Convert MIDI note number to frequency
|
||||
g_note_frequency = 440.0 * pow(2.0, (note - 69.0) / 12.0);
|
||||
g_note_on = true;
|
||||
g_phase = 0.0; // Reset phase for a clean attack
|
||||
}
|
||||
|
||||
void handleNoteOff(byte channel, byte note, byte velocity) {
|
||||
g_note_on = false;
|
||||
g_note_frequency = 0.0;
|
||||
// Write the same sample to both left and right channels (mono audio).
|
||||
// This call is blocking and will wait until there is space in the DMA buffer.
|
||||
i2s.write(sample);
|
||||
i2s.write(sample);
|
||||
}
|
||||
Reference in New Issue
Block a user