Compare commits

..

No commits in common. "2b6e1d2c6be57ab88fcd5218c82c7364a040ef5f" and "659926986b5eabc2d976575c968988577f51534b" have entirely different histories.

11 changed files with 13 additions and 606 deletions

2
.gitignore vendored
View File

@ -1,2 +0,0 @@
noicesynth_linux
miniaudio.h

View File

@ -17,6 +17,12 @@ const int16_t AMPLITUDE = 16383; // Use a lower amplitude to avoid clipping (max
I2S i2s(OUTPUT); I2S i2s(OUTPUT);
// --- Synthesizer State --- // --- 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
};
const int NUM_NOTES = sizeof(NOTE_FREQUENCIES) / sizeof(NOTE_FREQUENCIES[0]);
float currentFrequency = 440.0f; float currentFrequency = 440.0f;
double phase = 0.0; double phase = 0.0;
unsigned long lastNoteChangeTime = 0; unsigned long lastNoteChangeTime = 0;
@ -44,39 +50,15 @@ void loopAudio() {
// Every 500ms, pick a new random note to play // Every 500ms, pick a new random note to play
if (now - lastNoteChangeTime > 500) { if (now - lastNoteChangeTime > 500) {
lastNoteChangeTime = now; lastNoteChangeTime = now;
int noteIndex = random(0, SCALES[currentScaleIndex].numNotes); int noteIndex = random(0, NUM_NOTES);
currentFrequency = NOTE_FREQUENCIES[noteIndex];
// Calculate frequency based on key, scale, and octave
const float baseFrequency = 261.63f; // C4
float keyFrequency = baseFrequency * pow(2.0f, currentKeyIndex / 12.0f);
int semitoneOffset = SCALES[currentScaleIndex].semitones[noteIndex];
currentFrequency = keyFrequency * pow(2.0f, semitoneOffset / 12.0f);
Serial.println("Playing note: " + String(currentFrequency) + " Hz"); Serial.println("Playing note: " + String(currentFrequency) + " Hz");
} }
// Generate the sine wave sample // Generate the sine wave sample
int16_t sample;
double phaseIncrement = 2.0 * M_PI * currentFrequency / SAMPLE_RATE; double phaseIncrement = 2.0 * M_PI * currentFrequency / SAMPLE_RATE;
phase = fmod(phase + phaseIncrement, 2.0 * M_PI); phase = fmod(phase + phaseIncrement, 2.0 * M_PI);
int16_t sample = static_cast<int16_t>(AMPLITUDE * sin(phase));
switch (currentWavetableIndex) {
case 0: // Sine
sample = static_cast<int16_t>(AMPLITUDE * sin(phase));
break;
case 1: // Square
sample = (phase < M_PI) ? AMPLITUDE : -AMPLITUDE;
break;
case 2: // Saw
sample = static_cast<int16_t>(AMPLITUDE * (1.0 - (phase / M_PI)));
break;
case 3: // Triangle
sample = static_cast<int16_t>(AMPLITUDE * (2.0 * fabs(phase / M_PI - 1.0) - 1.0));
break;
default:
sample = 0;
break;
}
// Write the same sample to both left and right channels (mono audio). // 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. // This call is blocking and will wait until there is space in the DMA buffer.

View File

@ -1,43 +0,0 @@
# NoiceSynth Linux Emulator
This folder contains a Linux-based prototype for the NoiceSynth engine. It allows you to develop and visualize the DSP code on a desktop computer before deploying it to the RP2040 hardware.
## Architecture
* **Engine (`synth_engine.cpp`):** The exact same C++ code used on the microcontroller. It uses fixed-point math (or integer-based phase accumulation) to generate audio.
* **Host (`main.cpp`):** A Linux wrapper that uses:
* **Miniaudio:** For cross-platform audio output.
* **SDL2:** For real-time oscilloscope visualization.
## Quick Start (Distrobox)
If you don't want to install dependencies manually, use the included script. It creates a lightweight container, installs the tools, and compiles the project.
1. Ensure you have `distrobox` and a container engine (Docker or Podman) installed.
2. Run the build script:
```bash
./compile_with_distrobox.sh
```
3. Run the synthesizer:
```bash
./noicesynth_linux
```
## Manual Build
If you prefer to build directly on your host machine:
1. **Install Dependencies:**
* **Debian/Ubuntu:** `sudo apt install build-essential libsdl2-dev wget`
* **Fedora:** `sudo dnf install gcc-c++ SDL2-devel wget`
* **Arch:** `sudo pacman -S base-devel sdl2 wget`
2. **Download Miniaudio:**
```bash
wget https://raw.githubusercontent.com/mackron/miniaudio/master/miniaudio.h
```
3. **Compile:**
```bash
make
```

View File

@ -1,18 +0,0 @@
# Compiler and flags
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -I. $(shell sdl2-config --cflags)
LDFLAGS = -ldl -lm -lpthread $(shell sdl2-config --libs)
# Source files
SRCS = main.cpp synth_engine.cpp
# Output binary
TARGET = noicesynth_linux
all: $(TARGET)
$(TARGET): $(SRCS)
$(CXX) $(CXXFLAGS) -o $(TARGET) $(SRCS) $(LDFLAGS)
clean:
rm -f $(TARGET)

View File

@ -2,31 +2,4 @@
volatile unsigned long lastLoop0Time = 0; volatile unsigned long lastLoop0Time = 0;
volatile unsigned long lastLoop1Time = 0; volatile unsigned long lastLoop1Time = 0;
volatile bool watchdogActive = false; volatile bool watchdogActive = false;
UIState currentState = UI_MENU;
volatile int menuSelection = 0;
const MenuItem MENU_ITEMS[] = {
{ "Scale Type", UI_EDIT_SCALE_TYPE },
{ "Scale Key", UI_EDIT_SCALE_KEY },
{ "Wavetable", UI_EDIT_WAVETABLE }
};
const int NUM_MENU_ITEMS = sizeof(MENU_ITEMS) / sizeof(MENU_ITEMS[0]);
const Scale SCALES[] = {
{ "Major", { 0, 2, 4, 5, 7, 9, 11, 12 }, 8 },
{ "Minor", { 0, 2, 3, 5, 7, 8, 10, 12 }, 8 },
{ "Pentatonic", { 0, 2, 4, 7, 9, 12, 14, 16 }, 8 },
{ "Blues", { 0, 3, 5, 6, 7, 10, 12, 14 }, 8 }
};
const int NUM_SCALES = sizeof(SCALES) / sizeof(SCALES[0]);
volatile int currentScaleIndex = 0;
const char* KEY_NAMES[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
const int NUM_KEYS = sizeof(KEY_NAMES) / sizeof(KEY_NAMES[0]);
volatile int currentKeyIndex = 0; // C
const char* WAVETABLE_NAMES[] = {"Sine", "Square", "Saw", "Triangle"};
const int NUM_WAVETABLES = sizeof(WAVETABLE_NAMES) / sizeof(WAVETABLE_NAMES[0]);
volatile int currentWavetableIndex = 0; // Sine

View File

@ -7,39 +7,4 @@ extern volatile unsigned long lastLoop0Time;
extern volatile unsigned long lastLoop1Time; extern volatile unsigned long lastLoop1Time;
extern volatile bool watchdogActive; extern volatile bool watchdogActive;
enum UIState {
UI_MENU,
UI_EDIT_SCALE_TYPE,
UI_EDIT_SCALE_KEY,
UI_EDIT_WAVETABLE
};
struct Scale {
const char* name;
const int semitones[8];
int numNotes;
};
struct MenuItem {
const char* label;
UIState editState;
};
extern UIState currentState;
extern const MenuItem MENU_ITEMS[];
extern const int NUM_MENU_ITEMS;
extern volatile int menuSelection;
extern const Scale SCALES[];
extern const int NUM_SCALES;
extern volatile int currentScaleIndex;
extern const char* KEY_NAMES[];
extern const int NUM_KEYS;
extern volatile int currentKeyIndex;
extern const char* WAVETABLE_NAMES[];
extern const int NUM_WAVETABLES;
extern volatile int currentWavetableIndex;
#endif // SHAREDSTATE_H #endif // SHAREDSTATE_H

View File

@ -1,188 +1,12 @@
#include "UIThread.h" #include "UIThread.h"
#include "SharedState.h" #include "SharedState.h"
#include <Arduino.h> #include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
// I2C Pins (GP4/GP5)
#define PIN_SDA 4
#define PIN_SCL 5
// Encoder Pins
#define PIN_ENC_CLK 12
#define PIN_ENC_DT 13
#define PIN_ENC_SW 14
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
volatile int8_t encoderDelta = 0;
static uint8_t prevNextCode = 0;
static uint16_t store = 0;
// Button state
static int lastButtonReading = HIGH;
static int currentButtonState = HIGH;
static unsigned long lastDebounceTime = 0;
static bool buttonClick = false;
void handleInput();
void drawUI();
// --- ENCODER INTERRUPT ---
// Robust Rotary Encoder reading
void readEncoder() {
static int8_t rot_enc_table[] = {0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0};
prevNextCode <<= 2;
if (digitalRead(PIN_ENC_DT)) prevNextCode |= 0x02;
if (digitalRead(PIN_ENC_CLK)) prevNextCode |= 0x01;
prevNextCode &= 0x0f;
// If valid state
if (rot_enc_table[prevNextCode]) {
store <<= 4;
store |= prevNextCode;
if ((store & 0xff) == 0x2b) encoderDelta--;
if ((store & 0xff) == 0x17) encoderDelta++;
}
}
void setupUI() { void setupUI() {
Wire.setSDA(PIN_SDA); // This is the UI thread, running on core 0. For this example, we do nothing here.
Wire.setSCL(PIN_SCL);
Wire.begin();
pinMode(PIN_ENC_CLK, INPUT_PULLUP);
pinMode(PIN_ENC_DT, INPUT_PULLUP);
pinMode(PIN_ENC_SW, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_ENC_CLK), readEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC_DT), readEncoder, CHANGE);
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
display.clearDisplay();
display.display();
}
void handleInput() {
// Handle Encoder Rotation
int rotation = 0;
noInterrupts();
rotation = encoderDelta;
encoderDelta = 0;
interrupts();
if (rotation != 0) {
switch (currentState) {
case UI_MENU:
menuSelection += rotation;
while (menuSelection < 0) menuSelection += NUM_MENU_ITEMS;
menuSelection %= NUM_MENU_ITEMS;
break;
case UI_EDIT_SCALE_TYPE:
currentScaleIndex += rotation;
while (currentScaleIndex < 0) currentScaleIndex += NUM_SCALES;
currentScaleIndex %= NUM_SCALES;
break;
case UI_EDIT_SCALE_KEY:
currentKeyIndex += rotation;
while (currentKeyIndex < 0) currentKeyIndex += NUM_KEYS;
currentKeyIndex %= NUM_KEYS;
break;
case UI_EDIT_WAVETABLE:
currentWavetableIndex += rotation;
while (currentWavetableIndex < 0) currentWavetableIndex += NUM_WAVETABLES;
currentWavetableIndex %= NUM_WAVETABLES;
break;
}
}
// Handle Button Click
int reading = digitalRead(PIN_ENC_SW);
if (reading != lastButtonReading) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > 50) {
if (reading != currentButtonState) {
currentButtonState = reading;
if (currentButtonState == LOW) {
buttonClick = true;
}
}
}
lastButtonReading = reading;
if (buttonClick) {
buttonClick = false;
if (currentState == UI_MENU) {
currentState = MENU_ITEMS[menuSelection].editState;
} else {
currentState = UI_MENU;
}
}
}
void drawUI() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
if (currentState == UI_MENU) {
for (int i = 0; i < NUM_MENU_ITEMS; i++) {
if (i == menuSelection) {
display.fillRect(0, i * 10, SCREEN_WIDTH, 10, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
} else {
display.setTextColor(SSD1306_WHITE);
}
display.setCursor(2, i * 10 + 1);
display.print(MENU_ITEMS[i].label);
display.print(": ");
// Display current value
switch (MENU_ITEMS[i].editState) {
case UI_EDIT_SCALE_TYPE: display.print(SCALES[currentScaleIndex].name); break;
case UI_EDIT_SCALE_KEY: display.print(KEY_NAMES[currentKeyIndex]); break;
case UI_EDIT_WAVETABLE: display.print(WAVETABLE_NAMES[currentWavetableIndex]); break;
default: break;
}
}
} else {
// In an edit screen
const char* title = MENU_ITEMS[menuSelection].label;
const char* value = "";
switch (currentState) {
case UI_EDIT_SCALE_TYPE: value = SCALES[currentScaleIndex].name; break;
case UI_EDIT_SCALE_KEY: value = KEY_NAMES[currentKeyIndex]; break;
case UI_EDIT_WAVETABLE: value = WAVETABLE_NAMES[currentWavetableIndex]; break;
default: break;
}
display.println(title);
display.drawLine(0, 10, SCREEN_WIDTH, 10, SSD1306_WHITE);
display.setCursor(10, 25);
display.setTextSize(2);
display.print(value);
display.setTextSize(1);
display.setCursor(0, 50);
display.println(F("(Press to confirm)"));
}
display.display();
} }
void loopUI() { void loopUI() {
handleInput(); // The loop on core 0 is responsible for updating the UI. In this simple example, it does nothing.
drawUI(); delay(100);
delay(20); // Prevent excessive screen refresh
} }

View File

@ -1,44 +0,0 @@
#!/bin/bash
set -e
# Configuration
CONTAINER_NAME="noicesynth-builder"
IMAGE="archlinux:latest"
# 1. Check for Distrobox
if ! command -v distrobox &> /dev/null; then
echo "Error: distrobox is not installed on your system."
exit 1
fi
# 2. Create Container (if it doesn't exist)
# We use Arch Linux for easy access to latest toolchains and SDL2
if ! distrobox list | grep -q "$CONTAINER_NAME"; then
echo "Creating container '$CONTAINER_NAME'..."
distrobox create --image "$IMAGE" --name "$CONTAINER_NAME" --yes
fi
# 3. Execute Build Inside Container
PROJECT_DIR=$(pwd)
echo "Entering container to build..."
distrobox enter "$CONTAINER_NAME" --additional-flags "--workdir $PROJECT_DIR" -- sh -c '
set -e # Ensure script exits on error inside the container too
# A. Install Dependencies (only if missing)
# We check for sdl2-config and wget to see if dev tools are present
if ! command -v sdl2-config &> /dev/null || ! command -v wget &> /dev/null; then
echo "Installing compiler and libraries..."
sudo pacman -Syu --noconfirm base-devel sdl2 wget
fi
# B. Download miniaudio.h (if missing)
if [ ! -f miniaudio.h ]; then
echo "Downloading miniaudio.h..."
wget https://raw.githubusercontent.com/mackron/miniaudio/master/miniaudio.h
fi
# C. Compile
echo "Compiling Project..."
make
'
echo "Build Success! Run ./noicesynth_linux to start the synth."

150
main.cpp
View File

@ -1,150 +0,0 @@
#define MINIAUDIO_IMPLEMENTATION
#include "miniaudio.h"
#include <SDL2/SDL.h>
#include <vector>
#include <atomic>
#include "synth_engine.h" // Include our portable engine
#include <stdio.h>
// --- Configuration ---
const uint32_t SAMPLE_RATE = 44100;
const uint32_t CHANNELS = 1; // Mono
const int WINDOW_WIDTH = 800;
const int WINDOW_HEIGHT = 600;
// --- Visualization Buffer ---
const size_t VIS_BUFFER_SIZE = 8192;
std::vector<int16_t> vis_buffer(VIS_BUFFER_SIZE, 0);
std::atomic<size_t> vis_write_index{0};
// --- Global Synth Engine Instance ---
// The audio callback needs access to our synth, so we make it global.
SynthEngine engine(SAMPLE_RATE);
/**
* @brief The audio callback function that miniaudio will call.
*
* This function acts as the bridge between the audio driver and our synth engine.
* It asks the engine to fill the audio buffer provided by the driver.
*/
void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) {
(void)pDevice; // Unused
(void)pInput; // Unused
// Cast the output buffer to the format our engine expects (int16_t).
int16_t* pOutputS16 = (int16_t*)pOutput;
// Tell our engine to process `frameCount` samples and fill the buffer.
engine.process(pOutputS16, frameCount);
// Copy to visualization buffer
size_t idx = vis_write_index.load(std::memory_order_relaxed);
for (ma_uint32 i = 0; i < frameCount; ++i) {
vis_buffer[idx] = pOutputS16[i];
idx = (idx + 1) % VIS_BUFFER_SIZE;
}
vis_write_index.store(idx, std::memory_order_relaxed);
}
int main(int argc, char* argv[]) {
(void)argc; (void)argv;
// --- Init SDL ---
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
printf("SDL could not initialize! SDL_Error: %s\n", SDL_GetError());
return -1;
}
SDL_Window* window = SDL_CreateWindow("NoiceSynth Scope", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_SHOWN);
if (!window) return -1;
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!renderer) return -1;
ma_device_config config = ma_device_config_init(ma_device_type_playback);
config.playback.format = ma_format_s16; // Must match our engine's output format
config.playback.channels = CHANNELS;
config.sampleRate = SAMPLE_RATE;
config.dataCallback = data_callback;
ma_device device;
if (ma_device_init(NULL, &config, &device) != MA_SUCCESS) {
printf("Failed to initialize playback device.\n");
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return -1;
}
printf("Device Name: %s\n", device.playback.name);
ma_device_start(&device);
// --- Main Loop ---
bool quit = false;
SDL_Event e;
while (!quit) {
while (SDL_PollEvent(&e) != 0) {
if (e.type == SDL_QUIT) {
quit = true;
}
}
// Clear screen
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
// Draw Waveform
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255); // Green
// Determine read position (snapshot atomic write index)
size_t write_idx = vis_write_index.load(std::memory_order_relaxed);
// Find trigger (zero crossing) to stabilize the display
// Look back from write_idx to find a stable point
size_t search_start_offset = 2000;
size_t read_idx = (write_idx + VIS_BUFFER_SIZE - search_start_offset) % VIS_BUFFER_SIZE;
// Simple trigger search: find crossing from negative to positive
for (size_t i = 0; i < 1000; ++i) {
int16_t s1 = vis_buffer[read_idx];
size_t next_idx = (read_idx + 1) % VIS_BUFFER_SIZE;
int16_t s2 = vis_buffer[next_idx];
if (s1 <= 0 && s2 > 0) {
read_idx = next_idx; // Found trigger
break;
}
read_idx = next_idx;
}
// Draw points
int prev_x = 0;
int prev_y = WINDOW_HEIGHT / 2;
for (int x = 0; x < WINDOW_WIDTH; ++x) {
int16_t sample = vis_buffer[read_idx];
read_idx = (read_idx + 1) % VIS_BUFFER_SIZE;
// Map 16-bit sample (-32768 to 32767) to screen height
// Invert Y because screen Y grows downwards
int y = WINDOW_HEIGHT / 2 - (sample * (WINDOW_HEIGHT / 2) / 32768);
if (x > 0) {
SDL_RenderDrawLine(renderer, prev_x, prev_y, x, y);
}
prev_x = x;
prev_y = y;
}
SDL_RenderPresent(renderer);
}
ma_device_uninit(&device);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}

View File

@ -1,35 +0,0 @@
#include "synth_engine.h"
SynthEngine::SynthEngine(uint32_t sampleRate)
: _sampleRate(sampleRate),
_phase(0),
_increment(0)
{
// Initialize with a default frequency
setFrequency(440.0f);
}
void SynthEngine::setFrequency(float freq) {
// Calculate the phase increment for a given frequency.
// The phase accumulator is a 32-bit unsigned integer (0 to 2^32 - 1).
// One full cycle of the accumulator represents one cycle of the waveform.
// increment = (frequency * 2^32) / sampleRate
// We use a 64-bit intermediate calculation to prevent overflow.
_increment = static_cast<uint32_t>((static_cast<uint64_t>(freq) << 32) / _sampleRate);
}
void SynthEngine::process(int16_t* buffer, uint32_t numFrames) {
for (uint32_t i = 0; i < numFrames; ++i) {
// 1. Advance the phase. Integer overflow automatically wraps it,
// which is exactly what we want for a continuous oscillator.
_phase += _increment;
// 2. Generate the sample. For a sawtooth wave, the sample value is
// directly proportional to the phase. We take the top 16 bits
// of the 32-bit phase accumulator to get a signed 16-bit sample.
int16_t sample = static_cast<int16_t>(_phase >> 16);
// 3. Write the sample to the buffer.
buffer[i] = sample;
}
}

View File

@ -1,45 +0,0 @@
#ifndef SYNTH_ENGINE_H
#define SYNTH_ENGINE_H
#include <stdint.h>
/**
* @class SynthEngine
* @brief A portable, platform-agnostic synthesizer engine.
*
* This class contains the core digital signal processing (DSP) logic.
* It has no dependencies on any specific hardware, OS, or audio API.
* It works by filling a provided buffer with 16-bit signed audio samples.
*
* The oscillator uses a 32-bit unsigned integer as a phase accumulator,
* which is highly efficient and avoids floating-point math in the audio loop,
* making it ideal for the RP2040.
*/
class SynthEngine {
public:
/**
* @brief Constructs the synthesizer engine.
* @param sampleRate The audio sample rate in Hz (e.g., 44100).
*/
SynthEngine(uint32_t sampleRate);
/**
* @brief Fills a buffer with audio samples. This is the main audio callback.
* @param buffer Pointer to the output buffer to be filled.
* @param numFrames The number of audio frames (samples) to generate.
*/
void process(int16_t* buffer, uint32_t numFrames);
/**
* @brief Sets the frequency of the oscillator.
* @param freq The frequency in Hz.
*/
void setFrequency(float freq);
private:
uint32_t _sampleRate;
uint32_t _phase; // Phase accumulator for the oscillator.
uint32_t _increment; // Phase increment per sample, determines frequency.
};
#endif // SYNTH_ENGINE_H