Initial commit
- Implemented simple synth - added instrument sine wave - added instrument square wave - implemented ADSR
This commit is contained in:
37
build.zig
Normal file
37
build.zig
Normal 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
15
build.zig.zon
Normal 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
211
src/main.zig
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user