diff --git a/AudioThread.cpp b/AudioThread.cpp index 4e6b1c2..68a57c0 100644 --- a/AudioThread.cpp +++ b/AudioThread.cpp @@ -1,133 +1,67 @@ #include "AudioThread.h" #include "SharedState.h" +#include +#include -// --- 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(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(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); } \ No newline at end of file diff --git a/AudioThread.h b/AudioThread.h index 3c44b14..78e6f8c 100644 --- a/AudioThread.h +++ b/AudioThread.h @@ -1,7 +1,7 @@ -#ifndef AUDIO_THREAD_H -#define AUDIO_THREAD_H +#ifndef AUDIOTHREAD_H +#define AUDIOTHREAD_H void setupAudio(); void loopAudio(); -#endif \ No newline at end of file +#endif // AUDIOTHREAD_H \ No newline at end of file diff --git a/SharedState.cpp b/SharedState.cpp index 5d524cf..ece37bc 100644 --- a/SharedState.cpp +++ b/SharedState.cpp @@ -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; \ No newline at end of file +volatile bool watchdogActive = false; \ No newline at end of file diff --git a/SharedState.h b/SharedState.h index ac159c8..e0e83b0 100644 --- a/SharedState.h +++ b/SharedState.h @@ -1,59 +1,10 @@ -#ifndef SHARED_STATE_H -#define SHARED_STATE_H +#ifndef SHAREDSTATE_H +#define SHAREDSTATE_H #include -#include -#include -#include -#include -// --- 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 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 \ No newline at end of file +#endif // SHAREDSTATE_H \ No newline at end of file diff --git a/UIThread.cpp b/UIThread.cpp index 0b38347..790e667 100644 --- a/UIThread.cpp +++ b/UIThread.cpp @@ -1,90 +1,12 @@ #include "UIThread.h" #include "SharedState.h" -#include - -// --- Local State --- -static int g_last_clk_state; - -// --- Forward Declarations --- -static void readEncoder(); -static void updateDisplay(); +#include 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(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); } \ No newline at end of file diff --git a/UIThread.h b/UIThread.h index 4bb7efa..9593e26 100644 --- a/UIThread.h +++ b/UIThread.h @@ -1,7 +1,7 @@ -#ifndef UI_THREAD_H -#define UI_THREAD_H +#ifndef UITHREAD_H +#define UITHREAD_H void setupUI(); void loopUI(); -#endif \ No newline at end of file +#endif // UITHREAD_H \ No newline at end of file