commit 48ee8c2c75c0838440687fe2b670d7c3b3364df3 Author: Anachronaut Date: Fri Mar 13 15:09:17 2026 -0400 Initial commit of project. diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..e25603b --- /dev/null +++ b/Readme.md @@ -0,0 +1,16 @@ +# soundThing + + A basic demonstration of generating sound samples and feeding an audio buffer, along with opening a MIDI port with ALSA, and playing back tones from the sound generator using MIDI events. + +## Building: + + Depends on Raylib and probably libasound2-dev, so install those if you don't have them already. + + 1) Clone the repository. + 2) run make from inside the repository directory. + +### Running and use: + + 1) Run make run from inside the repository directory. + 2) Specify the client and port for the MIDI device you want to use to control soundThing. + 3) You should now be able to play back notes by sending soundThing MIDI data. Use the up and down arrows to change waveforms. diff --git a/include/midi.h b/include/midi.h new file mode 100644 index 0000000..a2616e0 --- /dev/null +++ b/include/midi.h @@ -0,0 +1,21 @@ +#ifndef MIDI_H +#define MIDI_H + +#define _POSIX_C_SOURCE 200809L +#include +#include "synth.h" + +typedef struct { + snd_seq_t *seq; + int port; + int client; + Synth *synth; // pointer to the synth we'll drive with MIDI events +} MidiState; + +int midiInit(MidiState *m, Synth *synth); +void midiListInputs(MidiState *m); +void midiConnect(MidiState *m, int srcClient, int srcPort); +void midiClose(MidiState *m); +void *midiThread(void *arg); + +#endif diff --git a/include/synth.h b/include/synth.h new file mode 100644 index 0000000..9e16732 --- /dev/null +++ b/include/synth.h @@ -0,0 +1,46 @@ +#ifndef SYNTH_H +#define SYNTH_H + +#include + +#define VOICE_COUNT 8 + +typedef enum { + WAVE_SINE, + WAVE_TRIANGLE, + WAVE_SAW, + WAVE_RAMP, + WAVE_PULSE, + WAVE_NOISE, + WAVE_COUNT // handy for the modulo wrap on waveform switching +} Waveform; + +typedef struct { + float phase; + float freqHz; + float amp; + float targetAmp; + int active; + int midiNote; +} Voice; + +typedef struct { + float sampleRate; + float attackSec; + float releaseSec; + float dutyCycle; + Waveform waveform; + Voice voices[VOICE_COUNT]; + int lastStolenVoice; + float pitchBend; // -1.0 to +1.0, normalized from raw MIDI value + float pitchBendRange; // semitones, e.g. 2.0f +} Synth; + +void synthInit(Synth *s, float sampleRate); +void synthNoteOn(Synth *s, int midiNote); +void synthNoteOff(Synth *s, int midiNote); +void synthFillBuffer(Synth *s, int16_t *out, int frames); +float waveformSample(Waveform w, float phase, float dutyCycle); +const char *waveformName(Waveform w); + +#endif diff --git a/makefile b/makefile new file mode 100644 index 0000000..08afca1 --- /dev/null +++ b/makefile @@ -0,0 +1,58 @@ +# Recursive Directory Digesting Makefile V1.0 +# === Project Settings === +PROJECT := soundThing # Change this to your project name +CC := gcc +CFLAGS := -Wall -Wextra -std=c11 -O2 +# Preprocessor flags (deps only here) +CPPFLAGS := -MMD -MP +# Add linker flags +LDFLAGS = -lraylib -lm -ldl -lpthread -lGL -lrt -lX11 -lasound #here (e.g. -lm) + +# === Directory Structure === +SRC_DIR := source +OBJ_DIR := build +BIN_DIR := bin +INC_DIR := include + +# === Discover headers recursively === +INC_SUBDIRS := $(shell find $(INC_DIR) -type d) +CPPFLAGS += $(addprefix -I,$(INC_SUBDIRS)) + +# === Discover sources recursively === +SRC := $(shell find $(SRC_DIR) -type f -name '*.c') +# Map source paths to object paths (mirror folders under build/) +OBJ := $(patsubst $(SRC_DIR)/%.c,$(OBJ_DIR)/%.o,$(SRC)) +DEP := $(OBJ:.o=.d) +BIN := $(BIN_DIR)/$(PROJECT) + +# === Rules === +.PHONY: all clean run dirs + +all: dirs $(BIN) + +dirs: + @mkdir -p $(BIN_DIR) + @mkdir -p $(sort $(dir $(OBJ))) # create object subdirs that mirror source/ + +# Link objects into final executable +$(BIN): $(OBJ) + $(CC) $(OBJ) -o $@ $(LDFLAGS) + @echo "Linked -> $@" + +# Compile each .c into a .o, with auto header deps +$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c + $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@ + @echo "Compiled -> $@" + +# Run the program +run: all + @echo "Running $(BIN)..." + @$(BIN) + +# Clean build artifacts +clean: + @rm -rf $(OBJ_DIR) $(BIN) + @echo "Clean complete." + +# Include auto-generated dependency files (if present) +-include $(DEP) diff --git a/source/midi.c b/source/midi.c new file mode 100644 index 0000000..db28f44 --- /dev/null +++ b/source/midi.c @@ -0,0 +1,111 @@ +#include "midi.h" +#include +#include +#include +#include + +extern atomic_int gRunning; + +int midiInit(MidiState *m, Synth *synth) +{ + if (snd_seq_open(&m->seq, "default", SND_SEQ_OPEN_INPUT | SND_SEQ_NONBLOCK, 0) < 0) { + fprintf(stderr, "Failed to open ALSA sequencer\n"); + return 0; + } + snd_seq_nonblock(m->seq, 1); + snd_seq_set_client_name(m->seq, "soundThing"); + m->client = snd_seq_client_id(m->seq); + m->port = snd_seq_create_simple_port(m->seq, "MIDI In", + SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, + SND_SEQ_PORT_TYPE_APPLICATION); + m->synth = synth; + return 1; +} + +void midiListInputs(MidiState *m) +{ + snd_seq_client_info_t *cinfo; + snd_seq_port_info_t *pinfo; + snd_seq_client_info_alloca(&cinfo); + snd_seq_port_info_alloca(&pinfo); + + printf("\nAvailable MIDI inputs:\n"); + snd_seq_client_info_set_client(cinfo, -1); + while (snd_seq_query_next_client(m->seq, cinfo) >= 0) { + int client = snd_seq_client_info_get_client(cinfo); + if (client == m->client) continue; + + snd_seq_port_info_set_client(pinfo, client); + snd_seq_port_info_set_port(pinfo, -1); + while (snd_seq_query_next_port(m->seq, pinfo) >= 0) { + unsigned int caps = snd_seq_port_info_get_capability(pinfo); + if (caps & SND_SEQ_PORT_CAP_READ && caps & SND_SEQ_PORT_CAP_SUBS_READ) { + printf(" [%d:%d] %s — %s\n", + client, + snd_seq_port_info_get_port(pinfo), + snd_seq_client_info_get_name(cinfo), + snd_seq_port_info_get_name(pinfo)); + } + } + } +} + +void midiConnect(MidiState *m, int srcClient, int srcPort) +{ + snd_seq_port_subscribe_t *sub; + snd_seq_addr_t sender, dest; + + sender.client = srcClient; + sender.port = srcPort; + dest.client = m->client; + dest.port = m->port; + + snd_seq_port_subscribe_alloca(&sub); + snd_seq_port_subscribe_set_sender(sub, &sender); + snd_seq_port_subscribe_set_dest(sub, &dest); + snd_seq_subscribe_port(m->seq, sub); + + printf("Connected [%d:%d] to Cricket\n", srcClient, srcPort); +} + +void midiClose(MidiState *m) +{ + snd_seq_close(m->seq); +} + +void *midiThread(void *arg) +{ + MidiState *m = (MidiState *)arg; + struct pollfd pfd; + snd_seq_poll_descriptors(m->seq, &pfd, 1, POLLIN); + + while (gRunning) { + int ready = poll(&pfd, 1, 50); + if (ready <= 0) continue; + + snd_seq_event_t *ev = NULL; + while (snd_seq_event_input(m->seq, &ev) >= 0 && ev) { + switch (ev->type) { + case SND_SEQ_EVENT_NOTEON: + if (ev->data.note.velocity == 0) + synthNoteOff(m->synth, ev->data.note.note); + else + synthNoteOn(m->synth, ev->data.note.note); + break; + case SND_SEQ_EVENT_NOTEOFF: + synthNoteOff(m->synth, ev->data.note.note); + break; + case SND_SEQ_EVENT_CONTROLLER: + if (ev->data.control.param == 70) + m->synth->waveform = ev->data.control.value % WAVE_COUNT; + break; + case SND_SEQ_EVENT_PITCHBEND: + m->synth->pitchBend = ev->data.control.value / 8191.0f; + break; + } + snd_seq_free_event(ev); + ev = NULL; + } + } + return NULL; +} diff --git a/source/soundThing.c b/source/soundThing.c new file mode 100644 index 0000000..2a8c442 --- /dev/null +++ b/source/soundThing.c @@ -0,0 +1,94 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include "raylib.h" +#include "synth.h" +#include "midi.h" + +atomic_int gRunning = 1; + +static Synth gSynth = {0}; +static MidiState gMidi = {0}; + +static void AudioOutputCallback(void *buffer, unsigned int frames) +{ + synthFillBuffer(&gSynth, (int16_t *)buffer, (int)frames); +} + +static void drawWaveform(Waveform w, float dutyCycle, int x, int y, int width, int height) +{ + int steps = width; + for (int i = 0; i < steps - 1; i++) { + float phase0 = (float)i / steps; + float phase1 = (float)(i + 1) / steps; + float s0 = waveformSample(w, phase0, dutyCycle); + float s1 = waveformSample(w, phase1, dutyCycle); + + int cx = x + i; + int cy = y + (int)((1.0f - (s0 * 0.5f + 0.5f)) * height); + int nx = x + i + 1; + int ny = y + (int)((1.0f - (s1 * 0.5f + 0.5f)) * height); + + DrawLine(cx, cy, nx, ny, GREEN); + } + DrawRectangleLines(x, y, width, height, DARKGRAY); +} + +int main(void) +{ + const int sampleRate = 48000; + + if (!midiInit(&gMidi, &gSynth)) return 1; + midiListInputs(&gMidi); + + int srcClient, srcPort; + printf("\nEnter client and port to connect (e.g. '20 0'), or -1 -1 to skip: "); + if (scanf("%d %d", &srcClient, &srcPort) == 2 && srcClient != -1) + midiConnect(&gMidi, srcClient, srcPort); + + pthread_t midiTid; + pthread_create(&midiTid, NULL, midiThread, &gMidi); + + synthInit(&gSynth, (float)sampleRate); + + InitWindow(800, 450, "SoundThing"); + InitAudioDevice(); + SetAudioStreamBufferSizeDefault(1024); + + AudioStream stream = LoadAudioStream(sampleRate, 16, 1); + SetAudioStreamCallback(stream, AudioOutputCallback); + PlayAudioStream(stream); + SetTargetFPS(120); + + while (!WindowShouldClose()) { + if (IsKeyPressed(KEY_UP)) + gSynth.waveform = (gSynth.waveform + 1) % WAVE_COUNT; + if (IsKeyPressed(KEY_DOWN)) + gSynth.waveform = (gSynth.waveform + WAVE_COUNT - 1) % WAVE_COUNT; + + BeginDrawing(); + ClearBackground(BLACK); + + drawWaveform(gSynth.waveform, gSynth.dutyCycle, 20, 120, 400, 150); + + DrawText("SoundThing", 20, 20, 24, RAYWHITE); + DrawText(TextFormat("Waveform: %s (UP/DOWN to change)", waveformName(gSynth.waveform)), + 20, 55, 20, RAYWHITE); + DrawText(TextFormat("MIDI: client %d port %d", gMidi.client, gMidi.port), + 20, 90, 20, GRAY); + + EndDrawing(); + } + + gRunning = 0; + StopAudioStream(stream); + UnloadAudioStream(stream); + CloseAudioDevice(); + CloseWindow(); + pthread_join(midiTid, NULL); + midiClose(&gMidi); + return 0; +} diff --git a/source/synth.c b/source/synth.c new file mode 100644 index 0000000..41e9af5 --- /dev/null +++ b/source/synth.c @@ -0,0 +1,139 @@ +#include "synth.h" +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +void synthInit(Synth *s, float sampleRate) +{ + s->sampleRate = sampleRate; + s->attackSec = 0.005f; + s->releaseSec = 0.050f; + s->dutyCycle = 0.5f; + s->waveform = WAVE_SINE; + s->lastStolenVoice = 0; + + for (int i = 0; i < VOICE_COUNT; i++) { + s->voices[i].phase = 0.0f; + s->voices[i].freqHz = 440.0f; + s->voices[i].amp = 0.0f; + s->voices[i].targetAmp = 0.0f; + s->voices[i].active = 0; + s->voices[i].midiNote = -1; + s->pitchBend = 0.0f; + s->pitchBendRange = 2.0f; + } +} + +float waveformSample(Waveform w, float phase, float dutyCycle) +{ + switch (w) { + case WAVE_SINE: + return sinf(2.0f * (float)M_PI * phase); + case WAVE_TRIANGLE: + return (phase < 0.5f) + ? ( 4.0f * phase - 1.0f) + : (-4.0f * phase + 3.0f); + case WAVE_SAW: + return 2.0f * phase - 1.0f; + case WAVE_RAMP: + return 1.0f - 2.0f * phase; + case WAVE_PULSE: + return (phase < dutyCycle) ? 1.0f : -1.0f; + case WAVE_NOISE: + return ((float)rand() / (float)RAND_MAX) * 2.0f - 1.0f; + default: + return 0.0f; + } +} + +const char *waveformName(Waveform w) +{ + switch (w) { + case WAVE_SINE: return "Sine"; + case WAVE_TRIANGLE: return "Triangle"; + case WAVE_SAW: return "Saw"; + case WAVE_RAMP: return "Ramp"; + case WAVE_PULSE: return "Pulse"; + case WAVE_NOISE: return "Noise"; + default: return "???"; + } +} + +void synthNoteOn(Synth *s, int midiNote) +{ + float hz = 440.0f * powf(2.0f, (midiNote - 69) / 12.0f); + + for (int i = 0; i < VOICE_COUNT; i++) { + if (!s->voices[i].active) { + s->voices[i].freqHz = hz; + s->voices[i].midiNote = midiNote; + s->voices[i].targetAmp = 1.0f; + s->voices[i].active = 1; + return; + } + } + + // No free voice — steal round-robin + int i = s->lastStolenVoice % VOICE_COUNT; + s->lastStolenVoice++; + s->voices[i].freqHz = hz; + s->voices[i].midiNote = midiNote; + s->voices[i].targetAmp = 1.0f; + s->voices[i].phase = 0.0f; + s->voices[i].active = 1; +} + +void synthNoteOff(Synth *s, int midiNote) +{ + for (int i = 0; i < VOICE_COUNT; i++) { + if (s->voices[i].active && s->voices[i].midiNote == midiNote) + s->voices[i].targetAmp = 0.0f; + } +} + +void synthFillBuffer(Synth *s, int16_t *out, int frames) +{ + const float sr = s->sampleRate; + const float attackInc = (s->attackSec <= 0.0f) ? 1.0f : (1.0f / (s->attackSec * sr)); + const float releaseInc = (s->releaseSec <= 0.0f) ? 1.0f : (1.0f / (s->releaseSec * sr)); + + for (int i = 0; i < frames; i++) { + float mix = 0.0f; + + for (int v = 0; v < VOICE_COUNT; v++) { + Voice *vv = &s->voices[v]; + if (!vv->active) continue; + + if (vv->targetAmp > vv->amp) { + vv->amp += attackInc; + if (vv->amp > vv->targetAmp) vv->amp = vv->targetAmp; + } else if (vv->targetAmp < vv->amp) { + vv->amp -= releaseInc; + if (vv->amp < vv->targetAmp) vv->amp = vv->targetAmp; + } + + if (vv->amp <= 0.0f && vv->targetAmp == 0.0f) { + vv->active = 0; + continue; + } + + //vv->phase += vv->freqHz / sr; + float bendMultiplier = powf(2.0f, (s->pitchBend * s->pitchBendRange) / 12.0f); + vv->phase += (vv->freqHz * bendMultiplier) / sr; + if (vv->phase >= 1.0f) vv->phase -= 1.0f; + + mix += waveformSample(s->waveform, vv->phase, s->dutyCycle) * vv->amp; + } + + mix *= (0.2f / VOICE_COUNT) * 4.0f; + + int32_t sample = (int32_t)lrintf(mix * 32767.0f); + if (sample > 32767) sample = 32767; + if (sample < -32768) sample = -32768; + out[i] = (int16_t)sample; + } +}