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