Initial commit of project.

This commit is contained in:
Anachronaut
2026-03-13 15:09:17 -04:00
commit 48ee8c2c75
7 changed files with 485 additions and 0 deletions

16
Readme.md Normal file
View File

@@ -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.

21
include/midi.h Normal file
View File

@@ -0,0 +1,21 @@
#ifndef MIDI_H
#define MIDI_H
#define _POSIX_C_SOURCE 200809L
#include <alsa/asoundlib.h>
#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

46
include/synth.h Normal file
View File

@@ -0,0 +1,46 @@
#ifndef SYNTH_H
#define SYNTH_H
#include <stdint.h>
#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

58
makefile Normal file
View File

@@ -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)

111
source/midi.c Normal file
View File

@@ -0,0 +1,111 @@
#include "midi.h"
#include <stdio.h>
#include <poll.h>
#include <stdatomic.h>
#include <alloca.h>
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;
}

94
source/soundThing.c Normal file
View File

@@ -0,0 +1,94 @@
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdatomic.h>
#include <pthread.h>
#include <alloca.h>
#include <alsa/asoundlib.h>
#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;
}

139
source/synth.c Normal file
View File

@@ -0,0 +1,139 @@
#include "synth.h"
#include <math.h>
#include <stdlib.h>
#include <stdint.h>
#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;
}
}