Initial commit of project.
This commit is contained in:
16
Readme.md
Normal file
16
Readme.md
Normal 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
21
include/midi.h
Normal 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
46
include/synth.h
Normal 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
58
makefile
Normal 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
111
source/midi.c
Normal 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
94
source/soundThing.c
Normal 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
139
source/synth.c
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user