/** * 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(); } }