Initial commit

- Implemented simple synth
- added instrument sine wave
- added instrument square wave
- implemented ADSR
This commit is contained in:
Hypercube
2026-08-14 22:12:17 +02:00
commit 1454566be6
3 changed files with 263 additions and 0 deletions

37
build.zig Normal file
View File

@@ -0,0 +1,37 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "synth",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
}),
});
exe.root_module.linkSystemLibrary("SDL3", .{ .preferred_link_mode = .dynamic });
b.installArtifact(exe);
const run_step = b.step("run", "Run the app");
const run_cmd = b.addRunArtifact(exe);
run_step.dependOn(&run_cmd.step);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_exe_tests.step);
}

15
build.zig.zon Normal file
View File

@@ -0,0 +1,15 @@
.{
.name = .synth,
.version = "0.0.0",
.fingerprint = 0x241431a6b2249dd3,
.minimum_zig_version = "0.16.0",
.dependencies = .{},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
},
}

211
src/main.zig Normal file
View File

@@ -0,0 +1,211 @@
const std = @import("std");
const sdl = @cImport({
@cInclude("SDL3/SDL.h");
});
const Note = struct {
start: usize,
end: usize,
frequency: f64,
attack: usize = 0,
decay: usize = 0,
decay_value: f32 = 0.6,
release: usize = 0,
base_fn: *const fn (t: f64) f64,
volume: f64 = 1,
key_id: u32 = 0,
};
fn semitoneFreq(n: f64) f64 {
return 440 * std.math.pow(f64, 2, (n - 49) / 12.0);
}
// TODO: add other octaves
const Tones = struct {
const C4: f64 = semitoneFreq(40);
const D4: f64 = semitoneFreq(42);
const E4: f64 = semitoneFreq(44);
const F4: f64 = semitoneFreq(45);
const G4: f64 = semitoneFreq(47);
const A4: f64 = semitoneFreq(49);
const H4: f64 = semitoneFreq(51);
};
fn sine_fn(t: f64) f64 {
return std.math.sin(t * std.math.pi * 2);
}
fn square_fn(t: f64) f64 {
if (@mod(t, 1) < 0.5) return 1;
return -1;
}
fn secondsToFrames() void {} // TODO: implement
fn sample_at_frame(frame: usize, samplerate: usize, note: Note) f32 {
if (frame < note.start or frame >= note.end +| note.release) return 0;
const samplerate_f: f64 = @floatFromInt(samplerate);
const frame_f: f64 = @floatFromInt(frame);
const time = frame_f / samplerate_f;
const value: f64 = note.base_fn(time * note.frequency);
var frames_since_start = frame - note.start;
var volume_modifier: f64 = note.decay_value;
if (frame >= note.end) {
frames_since_start = note.end - note.start;
}
if (frames_since_start < note.attack) {
const t: f64 = @floatFromInt(frames_since_start);
const attack_f: f64 = @floatFromInt(note.attack);
volume_modifier = std.math.lerp(0, 1, t / attack_f);
} else if (frames_since_start - note.attack < note.decay) {
const t: f64 = @floatFromInt(frames_since_start - note.attack);
const decay_f: f64 = @floatFromInt(note.decay);
volume_modifier = std.math.lerp(1, note.decay_value, t / decay_f);
}
if (frame >= note.end and frame < note.end + note.release) {
const t: f64 = @floatFromInt(frame - note.end);
const release_f: f64 = @floatFromInt(note.release);
volume_modifier = std.math.lerp(volume_modifier, 0, t / release_f);
}
return @floatCast(value * volume_modifier * note.volume);
}
const CallbackData = struct {
allocator: std.mem.Allocator,
frame: usize,
notes: std.ArrayList(Note),
};
fn callback(userdata: ?*anyopaque, stream: ?*sdl.SDL_AudioStream, _: c_int, total_amount: c_int) callconv(.c) void {
const data: ?*CallbackData = @ptrCast(@alignCast(userdata));
const allocator = data.?.allocator;
const buff = allocator.alloc(f32, @intCast(@divExact(total_amount, @sizeOf(f32)))) catch {
std.debug.print("alloc failed", .{});
return;
};
defer allocator.free(buff);
for (buff) |*value| {
value.* = 0;
}
var note_idx = data.?.notes.items.len;
while (note_idx > 0) {
note_idx -= 1;
const note = data.?.notes.items[note_idx];
if (data.?.frame >= note.end +| note.release) {
_ = data.?.notes.swapRemove(note_idx);
}
for (buff, 0..) |*value, i| {
value.* += sample_at_frame(data.?.frame + i, 48000, note);
}
}
data.?.frame += buff.len;
_ = sdl.SDL_PutAudioStreamData(stream, @ptrCast(buff), total_amount);
}
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
if (!sdl.SDL_Init(sdl.SDL_INIT_VIDEO | sdl.SDL_INIT_AUDIO)) {
std.debug.print("SDL_Init failed: {s}\n", .{sdl.SDL_GetError()});
return error.SDL_Init;
}
defer sdl.SDL_Quit();
const window = sdl.SDL_CreateWindow("synth", 800, 600, sdl.SDL_WINDOW_RESIZABLE) orelse return error.SDL_CreateWindow;
defer sdl.SDL_DestroyWindow(window);
const renderer = sdl.SDL_CreateRenderer(window, null) orelse return error.SDL_CreateRenderer;
defer sdl.SDL_DestroyRenderer(renderer);
var spec: sdl.SDL_AudioSpec = .{
.format = sdl.SDL_AUDIO_F32,
.channels = 1,
.freq = 48000,
};
var userdata: CallbackData = .{
.allocator = init.gpa,
.frame = 0,
.notes = .empty,
};
defer userdata.notes.deinit(allocator);
const stream = sdl.SDL_OpenAudioDeviceStream(sdl.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, callback, &userdata) orelse return error.SDL_OpenAudioDeviceStream;
defer sdl.SDL_DestroyAudioStream(stream);
_ = sdl.SDL_ResumeAudioStreamDevice(stream);
_ = sdl.SDL_SetRenderDrawColor(renderer, 0x18, 0x18, 0x18, 0xff);
outer: while (true) {
var event: sdl.SDL_Event = undefined;
while (sdl.SDL_PollEvent(&event)) {
switch (event.type) {
sdl.SDL_EVENT_QUIT => {
break :outer;
},
sdl.SDL_EVENT_KEY_DOWN => {
if (event.key.repeat) continue;
const simple_note: Note = .{
.base_fn = &sine_fn,
.start = userdata.frame,
.end = ~@as(usize, 0),
.frequency = Tones.A4,
.attack = 48000 * 0.2,
.decay = 48000 * 0.2,
.decay_value = 0.6,
.release = 48000,
.volume = 0.2,
.key_id = 0,
};
switch (event.key.key) {
sdl.SDLK_C => {
try userdata.notes.append(allocator, simple_note);
userdata.notes.items[userdata.notes.items.len - 1].key_id = sdl.SDLK_C;
userdata.notes.items[userdata.notes.items.len - 1].frequency = Tones.C4;
},
sdl.SDLK_D => {
try userdata.notes.append(allocator, simple_note);
userdata.notes.items[userdata.notes.items.len - 1].key_id = sdl.SDLK_D;
userdata.notes.items[userdata.notes.items.len - 1].frequency = Tones.D4;
},
sdl.SDLK_E => {
try userdata.notes.append(allocator, simple_note);
userdata.notes.items[userdata.notes.items.len - 1].key_id = sdl.SDLK_E;
userdata.notes.items[userdata.notes.items.len - 1].frequency = Tones.E4;
},
sdl.SDLK_F => {
try userdata.notes.append(allocator, simple_note);
userdata.notes.items[userdata.notes.items.len - 1].key_id = sdl.SDLK_F;
userdata.notes.items[userdata.notes.items.len - 1].frequency = Tones.F4;
},
sdl.SDLK_G => {
try userdata.notes.append(allocator, simple_note);
userdata.notes.items[userdata.notes.items.len - 1].key_id = sdl.SDLK_G;
userdata.notes.items[userdata.notes.items.len - 1].frequency = Tones.G4;
},
sdl.SDLK_A => {
try userdata.notes.append(allocator, simple_note);
userdata.notes.items[userdata.notes.items.len - 1].key_id = sdl.SDLK_A;
userdata.notes.items[userdata.notes.items.len - 1].frequency = Tones.A4;
},
sdl.SDLK_H => {
try userdata.notes.append(allocator, simple_note);
userdata.notes.items[userdata.notes.items.len - 1].key_id = sdl.SDLK_H;
userdata.notes.items[userdata.notes.items.len - 1].frequency = Tones.H4;
},
else => {},
}
},
sdl.SDL_EVENT_KEY_UP => {
for (userdata.notes.items) |*note| {
if (note.key_id == event.key.key and note.end > userdata.frame) {
note.end = userdata.frame;
}
}
},
else => {},
}
_ = sdl.SDL_RenderClear(renderer);
_ = sdl.SDL_RenderPresent(renderer);
sdl.SDL_Delay(10);
}
}
}