Playing notes over I2S

This commit is contained in:
Dejvino 2026-02-27 00:02:56 +01:00
parent 09a2d83c49
commit 659926986b
6 changed files with 61 additions and 268 deletions

View File

@ -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);
}

View File

@ -1,7 +1,7 @@
#ifndef AUDIO_THREAD_H
#define AUDIO_THREAD_H
#ifndef AUDIOTHREAD_H
#define AUDIOTHREAD_H
void setupAudio();
void loopAudio();
#endif
#endif // AUDIOTHREAD_H

View File

@ -1,19 +1,5 @@
#include "SharedState.h"
// --- Global Objects ---
I2S i2s;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// --- Watchdog ---
volatile unsigned long lastLoop0Time = 0;
volatile unsigned long lastLoop1Time = 0;
volatile bool watchdogActive = false;
// --- Synthesizer State ---
volatile float g_note_frequency = 0.0;
volatile bool g_note_on = false;
volatile float g_volume = 0.5;
float g_phase = 0.0;
// --- Control State ---
int g_encoder_value = 0;

View File

@ -1,59 +1,10 @@
#ifndef SHARED_STATE_H
#define SHARED_STATE_H
#ifndef SHAREDSTATE_H
#define SHAREDSTATE_H
#include <Arduino.h>
#include <I2S.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <MIDI.h>
// --- Pin Definitions ---
// I2S Audio
#define I2S_BCLK_PIN 9
#define I2S_LRCK_PIN 10
#define I2S_DATA_PIN 11
// I2C OLED Display
#define OLED_SDA_PIN 4
#define OLED_SCL_PIN 5
// Controls
#define ENCODER_CLK_PIN 12
#define ENCODER_DT_PIN 13
#define ENCODER_SW_PIN 14
#define VOL_POT_PIN 26 // ADC0
// MIDI Input
#define MIDI_RX_PIN 1 // UART0 RX
// --- Constants ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SAMPLE_RATE 44100
#define BITS_PER_SAMPLE 16
// --- Global Objects ---
extern I2S i2s;
extern Adafruit_SSD1306 display;
extern midi::MidiInterface<HardwareSerial> MIDI;
// --- Watchdog ---
extern volatile unsigned long lastLoop0Time;
extern volatile unsigned long lastLoop1Time;
extern volatile bool watchdogActive;
// --- Synthesizer State ---
extern volatile float g_note_frequency;
extern volatile bool g_note_on;
extern volatile float g_volume;
extern float g_phase;
// --- Control State ---
extern int g_encoder_value;
// --- Test Mode ---
// #define TEST_OUT
#endif
#endif // SHAREDSTATE_H

View File

@ -1,90 +1,12 @@
#include "UIThread.h"
#include "SharedState.h"
#include <Wire.h>
// --- Local State ---
static int g_last_clk_state;
// --- Forward Declarations ---
static void readEncoder();
static void updateDisplay();
#include <Arduino.h>
void setupUI() {
// --- Initialize I2C and Display ---
Wire.setSDA(OLED_SDA_PIN);
Wire.setSCL(OLED_SCL_PIN);
Wire.begin();
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x64
Serial.println(F("SSD1306 allocation failed"));
for (;;); // Don't proceed, loop forever
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.println("NoiceSynth Booting...");
display.display();
delay(1000);
// --- Initialize Controls ---
pinMode(ENCODER_CLK_PIN, INPUT_PULLUP);
pinMode(ENCODER_DT_PIN, INPUT_PULLUP);
pinMode(ENCODER_SW_PIN, INPUT_PULLUP);
g_last_clk_state = digitalRead(ENCODER_CLK_PIN);
// This is the UI thread, running on core 0. For this example, we do nothing here.
}
void loopUI() {
// Read the volume potentiometer (Pico ADC is 12-bit, 0-4095)
// We use a bit of filtering by averaging with the old value to reduce noise.
float new_volume = analogRead(VOL_POT_PIN) / 4095.0f;
g_volume = (g_volume * 0.95) + (new_volume * 0.05);
// Read the rotary encoder
readEncoder();
// Update the display periodically
static uint32_t last_display_update = 0;
if (millis() - last_display_update > 100) { // Update 10 times/sec
last_display_update = millis();
updateDisplay();
}
}
static void updateDisplay() {
display.clearDisplay();
display.setCursor(0, 0);
display.println(F(" NoiceSynth"));
display.println(F("--------------------"));
if (g_note_on) {
display.print(F("Note Freq: "));
display.print(g_note_frequency, 2);
display.println(F(" Hz"));
} else {
display.println(F("Note: Off"));
}
display.print(F("Volume: "));
display.print(static_cast<int>(g_volume * 100));
display.println(F("%"));
display.print(F("Encoder: "));
display.println(g_encoder_value);
display.display();
}
static void readEncoder() {
int new_clk_state = digitalRead(ENCODER_CLK_PIN);
// Check for a change on the CLK pin (a "tick" of the encoder)
if (new_clk_state != g_last_clk_state && new_clk_state == LOW) {
// Read the DT pin to determine direction
if (digitalRead(ENCODER_DT_PIN) == LOW) {
g_encoder_value++; // Clockwise
} else {
g_encoder_value--; // Counter-clockwise
}
}
g_last_clk_state = new_clk_state;
// The loop on core 0 is responsible for updating the UI. In this simple example, it does nothing.
delay(100);
}

View File

@ -1,7 +1,7 @@
#ifndef UI_THREAD_H
#define UI_THREAD_H
#ifndef UITHREAD_H
#define UITHREAD_H
void setupUI();
void loopUI();
#endif
#endif // UITHREAD_H