Refactoring
This commit is contained in:
parent
f7a93ee5bd
commit
09a2d83c49
133
AudioThread.cpp
Normal file
133
AudioThread.cpp
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
#include "AudioThread.h"
|
||||||
|
#include "SharedState.h"
|
||||||
|
|
||||||
|
// --- MIDI ---
|
||||||
|
// Create a MIDI object listening on Serial1 (GP0/GP1)
|
||||||
|
MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI);
|
||||||
|
|
||||||
|
// --- 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);
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
|
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 ---
|
||||||
|
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 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
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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;
|
||||||
|
}
|
||||||
7
AudioThread.h
Normal file
7
AudioThread.h
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#ifndef AUDIO_THREAD_H
|
||||||
|
#define AUDIO_THREAD_H
|
||||||
|
|
||||||
|
void setupAudio();
|
||||||
|
void loopAudio();
|
||||||
|
|
||||||
|
#endif
|
||||||
35
README.md
35
README.md
@ -24,23 +24,41 @@ This guide provides the blueprint for building your own.
|
|||||||
| **Power** | 3.7V LiPo Battery and a 5V booster/charger board (e.g., Adafruit PowerBoost). |
|
| **Power** | 3.7V LiPo Battery and a 5V booster/charger board (e.g., Adafruit PowerBoost). |
|
||||||
| **MIDI Circuit Parts** | 6N138 or 6N137 Optocoupler, 1N4148 Diode, various resistors (see diagram). |
|
| **MIDI Circuit Parts** | 6N138 or 6N137 Optocoupler, 1N4148 Diode, various resistors (see diagram). |
|
||||||
|
|
||||||
## Powering Your Synth (Battery Operation)
|
## Powering Your Synth
|
||||||
|
|
||||||
To make the synth truly portable, we'll use a LiPo battery and a booster board. This is critical for providing stable power to all components.
|
You can power the synth either from a portable LiPo battery or directly from USB.
|
||||||
|
|
||||||
|
### Battery Operation (Portable)
|
||||||
|
|
||||||
|
To make the synth truly portable, use a LiPo battery and a 5V booster/charger board. This provides stable power to all components.
|
||||||
|
|
||||||
> **LIPO BATTERY WARNING**: LiPo batteries are powerful but require careful handling.
|
> **LIPO BATTERY WARNING**: LiPo batteries are powerful but require careful handling.
|
||||||
> * **Never** use a LiPo battery without a dedicated protection and charging circuit like the PowerBoost.
|
> * **Never** use a LiPo battery without a dedicated protection and charging circuit like the PowerBoost.
|
||||||
> * Do not puncture, bend, or short-circuit a LiPo battery.
|
> * Do not puncture, bend, or short-circuit a LiPo battery.
|
||||||
|
|
||||||
### Wiring for Power:
|
|
||||||
|
|
||||||
1. Connect the **LiPo battery** to the JST connector on the **PowerBoost board**.
|
1. Connect the **LiPo battery** to the JST connector on the **PowerBoost board**.
|
||||||
2. Connect the **PowerBoost's 5V output** to the **Pico's VBUS pin (Pin 40)**. This powers the Pico.
|
2. Connect the **PowerBoost's 5V output** to the **Pico's VBUS pin (Pin 40)**. This powers the Pico.
|
||||||
3. Connect the **PowerBoost's 5V output** to the **VCC/VIN pin of your I2S Audio Module**.
|
3. Connect the **PowerBoost's 5V output** to the **VCC/VIN pin of your I2S Audio Module**.
|
||||||
4. Connect the **PowerBoost's GND** to one of the **Pico's GND pins**.
|
4. Connect the **PowerBoost's GND** to one of the **Pico's GND pins**. This creates a common ground.
|
||||||
5. Connect this **common ground** to the **GND pins of all other components** (OLED, I2S Module, Encoder, Potentiometer, MIDI circuit).
|
|
||||||
|
|
||||||
This setup ensures everything shares a common ground and that the power-hungry components get the stable 5V they need, while the Pico's onboard regulator provides 3.3V for the lower-power parts.
|
### USB Operation
|
||||||
|
|
||||||
|
If you don't need battery power, you can run the synth from a USB source (like a computer or wall adapter). The wiring is simpler.
|
||||||
|
|
||||||
|
There are two ways to power the components:
|
||||||
|
|
||||||
|
**Option 1 (Simplest): Power everything from 3.3V**
|
||||||
|
The PCM5102A DAC chip runs natively on 3.3V. This is the easiest and safest wiring method for USB operation.
|
||||||
|
1. Power the Pico by connecting its **micro-USB port** to a USB power source.
|
||||||
|
2. Connect the Pico's **3V3(OUT) pin (Pin 36)** to the VCC/VIN pins of **all** components: the I2S Module, OLED, Rotary Encoder, Potentiometer, and MIDI circuit.
|
||||||
|
3. Ensure all components share a **common ground** with one of the Pico's GND pins.
|
||||||
|
|
||||||
|
**Option 2: Power I2S Module from 5V (with protection)**
|
||||||
|
If you prefer to give the audio module a separate 5V supply from USB. For good measure, adding a Schottky diode (e.g., 1N5817) is recommended to protect against any potential reverse voltage.
|
||||||
|
1. Power the Pico via its **micro-USB port**.
|
||||||
|
2. Connect the Pico's **VBUS pin (Pin 40)** to the **anode (+)** of a Schottky diode. Connect the **cathode (-)** of the diode to the **VCC/VIN pin of your I2S Audio Module**.
|
||||||
|
3. Connect the Pico's **3V3(OUT) pin (Pin 36)** to power the OLED, Encoder, Potentiometer, and MIDI circuit.
|
||||||
|
4. Ensure all components share a **common ground**.
|
||||||
|
|
||||||
## Wiring & Connections
|
## Wiring & Connections
|
||||||
|
|
||||||
@ -49,11 +67,12 @@ Establish a **common ground** by connecting the ground from your PowerBoost boar
|
|||||||
| Component | Pico Pin | Description |
|
| Component | Pico Pin | Description |
|
||||||
| ------------------ | ------------------ | ---------------------------------------------- |
|
| ------------------ | ------------------ | ---------------------------------------------- |
|
||||||
| **I2S Audio Module** | | |
|
| **I2S Audio Module** | | |
|
||||||
| VCC | 5V from PowerBoost | Power for the amplifier |
|
| VCC | 3.3V or 5V Source (see Power section) | Power for the amplifier |
|
||||||
| GND | Common GND | Ground |
|
| GND | Common GND | Ground |
|
||||||
| DIN (Data) | GP11 (Pin 15) | I2S Data Out |
|
| DIN (Data) | GP11 (Pin 15) | I2S Data Out |
|
||||||
| BCLK (Bit Clock) | GP9 (Pin 12) | I2S Bit Clock |
|
| BCLK (Bit Clock) | GP9 (Pin 12) | I2S Bit Clock |
|
||||||
| LRCK (Word Clock) | GP10 (Pin 14) | I2S Left/Right Clock |
|
| LRCK (Word Clock) | GP10 (Pin 14) | I2S Left/Right Clock |
|
||||||
|
| SCK (System Clock) | GND | **PCM5102 Only**: Connect to GND for internal PLL |
|
||||||
| **SSD1306 OLED** | | |
|
| **SSD1306 OLED** | | |
|
||||||
| VCC | 3V3 (OUT) (Pin 36) | 3.3V Power |
|
| VCC | 3V3 (OUT) (Pin 36) | 3.3V Power |
|
||||||
| GND | Common GND | Ground |
|
| GND | Common GND | Ground |
|
||||||
|
|||||||
@ -1,275 +1,45 @@
|
|||||||
/**
|
#include "SharedState.h"
|
||||||
* NoiceSynth - A Compact RP2040 Synthesizer
|
#include "UIThread.h"
|
||||||
*
|
#include "AudioThread.h"
|
||||||
* 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 <I2S.h>
|
|
||||||
#include <Wire.h>
|
|
||||||
#include <Adafruit_GFX.h>
|
|
||||||
#include <Adafruit_SSD1306.h>
|
|
||||||
#include <MIDI.h>
|
|
||||||
|
|
||||||
// --- 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<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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 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<int>(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() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
|
delay(2000); // Wait for serial monitor
|
||||||
|
Serial.println(F("Starting NoiceSynth..."));
|
||||||
|
|
||||||
// --- Initialize I2C and Display ---
|
setupUI();
|
||||||
Wire.setSDA(OLED_SDA_PIN);
|
setupAudio();
|
||||||
Wire.setSCL(OLED_SCL_PIN);
|
|
||||||
Wire.begin();
|
|
||||||
|
|
||||||
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x64
|
Serial.println(F("Started."));
|
||||||
Serial.println(F("SSD1306 allocation failed"));
|
|
||||||
for (;;); // Don't proceed, loop forever
|
// Enable Watchdog
|
||||||
|
lastLoop0Time = millis();
|
||||||
|
lastLoop1Time = millis();
|
||||||
|
watchdogActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop1() {
|
||||||
|
if (!watchdogActive) {
|
||||||
|
delay(100);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
display.clearDisplay();
|
unsigned long now = millis();
|
||||||
display.setTextColor(SSD1306_WHITE);
|
lastLoop1Time = now;
|
||||||
display.setTextSize(1);
|
if (watchdogActive && (now - lastLoop0Time > 1000)) {
|
||||||
display.println("NoiceSynth Booting...");
|
Serial.println(F("Core 0 Freeze detected! Rebooting..."));
|
||||||
display.display();
|
rp2040.reboot();
|
||||||
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.");
|
|
||||||
|
loopAudio();
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
#ifdef TEST_OUT
|
unsigned long now = millis();
|
||||||
static uint32_t last_note_event = 0;
|
lastLoop0Time = now;
|
||||||
static bool is_playing_test_note = false;
|
if (watchdogActive && (now - lastLoop1Time > 1000)) {
|
||||||
const uint16_t note_duration = 250; // ms
|
Serial.println(F("Core 1 Freeze detected! Rebooting..."));
|
||||||
const uint16_t note_gap = 50; // ms
|
rp2040.reboot();
|
||||||
|
|
||||||
// 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
|
loopUI();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
19
SharedState.cpp
Normal file
19
SharedState.cpp
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#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;
|
||||||
59
SharedState.h
Normal file
59
SharedState.h
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
#ifndef SHARED_STATE_H
|
||||||
|
#define SHARED_STATE_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
|
||||||
90
UIThread.cpp
Normal file
90
UIThread.cpp
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
#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();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
7
UIThread.h
Normal file
7
UIThread.h
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#ifndef UI_THREAD_H
|
||||||
|
#define UI_THREAD_H
|
||||||
|
|
||||||
|
void setupUI();
|
||||||
|
void loopUI();
|
||||||
|
|
||||||
|
#endif
|
||||||
Loading…
Reference in New Issue
Block a user