diff --git a/RP2040_NoiceSynth.ino b/RP2040_NoiceSynth.ino new file mode 100644 index 0000000..25a34a3 --- /dev/null +++ b/RP2040_NoiceSynth.ino @@ -0,0 +1,275 @@ +/** + * NoiceSynth - A Compact RP2040 Synthesizer + * + * This sketch provides the foundational code for a portable MIDI synthesizer + * based on the Raspberry Pi Pico (RP2040). + * + * Features implemented: + * - I2S audio output for a simple sawtooth oscillator. + * - MIDI input handling (Note On/Off) via TRS jack to control the oscillator. + * - OLED display for visual feedback (current note, volume). + * - Analog volume control via a potentiometer. + * - Basic rotary encoder reading (framework for future parameter control). + * + * Libraries Required (Install via Arduino Library Manager): + * - Adafruit GFX Library + * - Adafruit SSD1306 + * - MIDI Library (by Forty Seven Effects) + * + * The I2S library by Earle F. Philhower, III is included with the RP2040 board core. + */ + +#include +#include +#include +#include +#include + +// --- Pin Definitions (as per README.md) --- +// 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 // Reset pin # (or -1 if sharing Arduino reset pin) + +#define SAMPLE_RATE 44100 +#define BITS_PER_SAMPLE 16 + +// --- Global Objects --- +I2S i2s; +Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); + +// Create a MIDI object listening on Serial1 (GP0/GP1) +MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI); + +// --- Synthesizer State Variables --- +volatile float g_note_frequency = 0.0; +volatile bool g_note_on = false; +volatile float g_volume = 0.5; + +#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 +}; +#endif + +// Oscillator phase +float g_phase = 0.0; + +// Rotary encoder state +int g_encoder_value = 0; +int g_last_clk_state; + +// --- 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. + for (size_t i = 0; i < buffer_size; i++) { + buffer[i] = 0; + } + 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; +} + +// --- Helper Functions --- +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(); +} + +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; +} + + +void setup() { + Serial.begin(115200); + + // --- 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); + +#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 --- + i2s.setBCLK(I2S_BCLK_PIN); + i2s.setLRCK(I2S_LRCK_PIN); + i2s.setDATA(I2S_DATA_PIN); + + // Set the audio callback function + i2s.setBufferCallback(fill_audio_buffer); + + if (!i2s.begin(I2S_STEREO, SAMPLE_RATE, BITS_PER_SAMPLE)) { + Serial.println("Failed to initialize I2S!"); + while (1); // Stop forever + } + Serial.println("I2S Initialized."); +} + +void loop() { +#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 + + // 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 + } + + // 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]; + + // 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 + + // 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(); + } +} \ No newline at end of file