commit 1454566be62467282d2db000e988f133907d8e86 Author: Hypercube Date: Fri Aug 14 22:12:17 2026 +0200 Initial commit - Implemented simple synth - added instrument sine wave - added instrument square wave - implemented ADSR diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..6a756ff --- /dev/null +++ b/build.zig @@ -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); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..cee9be1 --- /dev/null +++ b/build.zig.zon @@ -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", + }, +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..f752975 --- /dev/null +++ b/src/main.zig @@ -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); + } + } +}