this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat!: Implement synthesized audio playpack; Rename MidiMessage to MidiEvent #2

open opened by danikvitek.eurosky.social targeting main from synth
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:bsvbcgos44pye2a3fz4iwovu/sh.tangled.repo.pull/3ml74t37ych22
+485 -26
Diff #2
+11
build.zig
··· 21 21 // target and optimize options) will be listed when running `zig build --help` 22 22 // in this directory. 23 23 24 + const translate_c = b.addTranslateC(.{ 25 + .root_source_file = b.path("src/alsa.h"), 26 + .optimize = optimize, 27 + .target = target, 28 + }); 29 + translate_c.linkSystemLibrary("asound", .{ .needed = true }); // change to false when going crossplatform 30 + 31 + const alsa_mod = translate_c.createModule(); 32 + 24 33 // This creates a module, which represents a collection of source files alongside 25 34 // some compilation options, such as optimization mode and linked system libraries. 26 35 // Zig modules are the preferred way of making Zig code available to consumers. ··· 40 49 // which requires us to specify a target. 41 50 .target = target, 42 51 }); 52 + mod.addImport("alsa-ffi", alsa_mod); 43 53 44 54 // Here we define an executable. An executable needs to have a root module 45 55 // which needs to expose a `main` function. While we could add a main function ··· 79 89 // can be extremely useful in case of collisions (which can happen 80 90 // importing modules from different packages). 81 91 .{ .name = "midi-synth", .module = mod }, 92 + .{ .name = "alsa-ffi", .module = alsa_mod }, 82 93 }, 83 94 }), 84 95 });
+1
src/alsa.h
··· 1 + #include <alsa/asoundlib.h>
+229
src/alsa.zig
··· 1 + const std = @import("std"); 2 + 3 + const alsa_ffi = @import("alsa-ffi"); 4 + pub const SndPcm = alsa_ffi.snd_pcm_t; 5 + pub const SndPcmHwParams = alsa_ffi.snd_pcm_hw_params_t; 6 + 7 + pub const SndPcmStream = enum(alsa_ffi.snd_pcm_stream_t) { 8 + playback = alsa_ffi.SND_PCM_STREAM_PLAYBACK, 9 + capture = alsa_ffi.SND_PCM_STREAM_CAPTURE, 10 + _, 11 + }; 12 + 13 + pub const SndPcmMode = enum(c_int) { 14 + /// The program will pause (block) and wait until space is available in the hardware buffer. 15 + // This is the easiest way to keep your synthesizer engine synchronized with the sound card. 16 + block = 0, 17 + /// If the buffer is full, the function instantly returns a -EAGAIN error instead of waiting. 18 + /// Useful for complex, multi-threaded audio servers. 19 + nonblock = alsa_ffi.SND_PCM_NONBLOCK, 20 + /// ALSA will send a Unix signal (like SIGIO) to your program when the buffer is ready. 21 + /// Rarely used in modern applications. 22 + async = alsa_ffi.SND_PCM_ASYNC, 23 + _, 24 + }; 25 + 26 + pub const SndPcmAccess = enum(c_int) { 27 + mmap_interleaved = alsa_ffi.SND_PCM_ACCESS_MMAP_INTERLEAVED, 28 + mmap_noninterleaved = alsa_ffi.SND_PCM_ACCESS_MMAP_NONINTERLEAVED, 29 + mmap_complex = alsa_ffi.SND_PCM_ACCESS_MMAP_COMPLEX, 30 + rw_interleaved = alsa_ffi.SND_PCM_ACCESS_RW_INTERLEAVED, 31 + rw_noninterleaved = alsa_ffi.SND_PCM_ACCESS_RW_NONINTERLEAVED, 32 + _, 33 + }; 34 + 35 + pub const SndPcmFormat = enum(c_int) { 36 + unknown = alsa_ffi.SND_PCM_FORMAT_UNKNOWN, 37 + s8 = alsa_ffi.SND_PCM_FORMAT_S8, 38 + u8 = alsa_ffi.SND_PCM_FORMAT_U8, 39 + s16_le = alsa_ffi.SND_PCM_FORMAT_S16_LE, 40 + s16_be = alsa_ffi.SND_PCM_FORMAT_S16_BE, 41 + u16_le = alsa_ffi.SND_PCM_FORMAT_U16_LE, 42 + u16_be = alsa_ffi.SND_PCM_FORMAT_U16_BE, 43 + s24_le = alsa_ffi.SND_PCM_FORMAT_S24_LE, 44 + s24_be = alsa_ffi.SND_PCM_FORMAT_S24_BE, 45 + u24_le = alsa_ffi.SND_PCM_FORMAT_U24_LE, 46 + u24_be = alsa_ffi.SND_PCM_FORMAT_U24_BE, 47 + s32_le = alsa_ffi.SND_PCM_FORMAT_S32_LE, 48 + s32_be = alsa_ffi.SND_PCM_FORMAT_S32_BE, 49 + u32_le = alsa_ffi.SND_PCM_FORMAT_U32_LE, 50 + u32_be = alsa_ffi.SND_PCM_FORMAT_U32_BE, 51 + float_le = alsa_ffi.SND_PCM_FORMAT_FLOAT_LE, 52 + float_be = alsa_ffi.SND_PCM_FORMAT_FLOAT_BE, 53 + float64_le = alsa_ffi.SND_PCM_FORMAT_FLOAT64_LE, 54 + float64_be = alsa_ffi.SND_PCM_FORMAT_FLOAT64_BE, 55 + iec958_subframe_le = alsa_ffi.SND_PCM_FORMAT_IEC958_SUBFRAME_LE, 56 + iec958_subframe_be = alsa_ffi.SND_PCM_FORMAT_IEC958_SUBFRAME_BE, 57 + mu_law = alsa_ffi.SND_PCM_FORMAT_MU_LAW, 58 + a_law = alsa_ffi.SND_PCM_FORMAT_A_LAW, 59 + ima_adpcm = alsa_ffi.SND_PCM_FORMAT_IMA_ADPCM, 60 + mpeg = alsa_ffi.SND_PCM_FORMAT_MPEG, 61 + gsm = alsa_ffi.SND_PCM_FORMAT_GSM, 62 + s20_le = alsa_ffi.SND_PCM_FORMAT_S20_LE, 63 + s20_be = alsa_ffi.SND_PCM_FORMAT_S20_BE, 64 + u20_le = alsa_ffi.SND_PCM_FORMAT_U20_LE, 65 + u20_be = alsa_ffi.SND_PCM_FORMAT_U20_BE, 66 + special = alsa_ffi.SND_PCM_FORMAT_SPECIAL, 67 + s24_3le = alsa_ffi.SND_PCM_FORMAT_S24_3LE, 68 + s24_3be = alsa_ffi.SND_PCM_FORMAT_S24_3BE, 69 + u24_3le = alsa_ffi.SND_PCM_FORMAT_U24_3LE, 70 + u24_3be = alsa_ffi.SND_PCM_FORMAT_U24_3BE, 71 + s20_3le = alsa_ffi.SND_PCM_FORMAT_S20_3LE, 72 + s20_3be = alsa_ffi.SND_PCM_FORMAT_S20_3BE, 73 + u20_3le = alsa_ffi.SND_PCM_FORMAT_U20_3LE, 74 + u20_3be = alsa_ffi.SND_PCM_FORMAT_U20_3BE, 75 + s18_3le = alsa_ffi.SND_PCM_FORMAT_S18_3LE, 76 + s18_3be = alsa_ffi.SND_PCM_FORMAT_S18_3BE, 77 + u18_3le = alsa_ffi.SND_PCM_FORMAT_U18_3LE, 78 + u18_3be = alsa_ffi.SND_PCM_FORMAT_U18_3BE, 79 + g723_24 = alsa_ffi.SND_PCM_FORMAT_G723_24, 80 + g723_24_1b = alsa_ffi.SND_PCM_FORMAT_G723_24_1B, 81 + g723_40 = alsa_ffi.SND_PCM_FORMAT_G723_40, 82 + g723_40_1b = alsa_ffi.SND_PCM_FORMAT_G723_40_1B, 83 + dsd_u8 = alsa_ffi.SND_PCM_FORMAT_DSD_U8, 84 + dsd_u16_le = alsa_ffi.SND_PCM_FORMAT_DSD_U16_LE, 85 + dsd_u32_le = alsa_ffi.SND_PCM_FORMAT_DSD_U32_LE, 86 + dsd_u16_be = alsa_ffi.SND_PCM_FORMAT_DSD_U16_BE, 87 + dsd_u32_be = alsa_ffi.SND_PCM_FORMAT_DSD_U32_BE, 88 + _, 89 + 90 + const Self = @This(); 91 + pub const s16: Self = @enumFromInt(alsa_ffi.SND_PCM_FORMAT_S16); 92 + pub const @"u16": Self = @enumFromInt(alsa_ffi.SND_PCM_FORMAT_U16); 93 + pub const s24: Self = @enumFromInt(alsa_ffi.SND_PCM_FORMAT_S24); 94 + pub const @"u24": Self = @enumFromInt(alsa_ffi.SND_PCM_FORMAT_U24); 95 + pub const s32: Self = @enumFromInt(alsa_ffi.SND_PCM_FORMAT_S32); 96 + pub const @"u32": Self = @enumFromInt(alsa_ffi.SND_PCM_FORMAT_U32); 97 + pub const float: Self = @enumFromInt(alsa_ffi.SND_PCM_FORMAT_FLOAT); 98 + pub const float64: Self = @enumFromInt(alsa_ffi.SND_PCM_FORMAT_FLOAT64); 99 + pub const iec958_subframe: Self = @enumFromInt(alsa_ffi.SND_PCM_FORMAT_IEC958_SUBFRAME); 100 + pub const s20: Self = @enumFromInt(alsa_ffi.SND_PCM_FORMAT_S20); 101 + pub const @"u20": Self = @enumFromInt(alsa_ffi.SND_PCM_FORMAT_U20); 102 + }; 103 + 104 + pub inline fn sndPcmOpen( 105 + name: [*:0]const u8, 106 + stream: SndPcmStream, 107 + mode: SndPcmMode, 108 + ) !*SndPcm { 109 + var pcm: ?*SndPcm = null; 110 + const code = alsa_ffi.snd_pcm_open( 111 + &pcm, 112 + name, 113 + @intFromEnum(stream), 114 + @intFromEnum(mode), 115 + ); 116 + _ = try check(code); 117 + return pcm.?; 118 + } 119 + 120 + pub inline fn sndPcmClose(pcm: *SndPcm) !void { 121 + _ = try check(alsa_ffi.snd_pcm_close(pcm)); 122 + } 123 + 124 + pub inline fn sndPcmHwParamsAlloc() !*SndPcmHwParams { 125 + var params: ?*SndPcmHwParams = null; 126 + const code = alsa_ffi.snd_pcm_hw_params_malloc(&params); 127 + _ = try check(code); 128 + return params.?; 129 + } 130 + 131 + pub inline fn sndPcmHwParamsFree(params: *SndPcmHwParams) void { 132 + alsa_ffi.snd_pcm_hw_params_free(params); 133 + } 134 + 135 + pub inline fn sndPcmHwParamsAny(pcm: *SndPcm, params: *SndPcmHwParams) !void { 136 + const code = alsa_ffi.snd_pcm_hw_params_any(pcm, params); 137 + _ = try check(code); 138 + } 139 + 140 + pub inline fn sndPcmHwParamsSetRateResample(pcm: *SndPcm, params: *SndPcmHwParams, val: bool) !void { 141 + const code = alsa_ffi.snd_pcm_hw_params_set_rate_resample(pcm, params, @intFromBool(val)); 142 + _ = try check(code); 143 + } 144 + 145 + pub inline fn sndPcmHwParamsSetAccess(pcm: *SndPcm, params: *SndPcmHwParams, access: SndPcmAccess) !void { 146 + const code = alsa_ffi.snd_pcm_hw_params_set_access(pcm, params, @intFromEnum(access)); 147 + _ = try check(code); 148 + } 149 + 150 + pub inline fn sndPcmHwParamsSetFormat(pcm: *SndPcm, params: *SndPcmHwParams, val: SndPcmFormat) !void { 151 + const code = alsa_ffi.snd_pcm_hw_params_set_format(pcm, params, @intFromEnum(val)); 152 + _ = try check(code); 153 + } 154 + 155 + pub inline fn sndPcmHwParamsSetChannels(pcm: *SndPcm, params: *SndPcmHwParams, val: c_uint) !void { 156 + const code = alsa_ffi.snd_pcm_hw_params_set_channels(pcm, params, val); 157 + _ = try check(code); 158 + } 159 + 160 + pub inline fn sndPcmHwParamsGetRateMax(params: *SndPcmHwParams, val: *c_uint, dir: *c_int) !void { 161 + const code = alsa_ffi.snd_pcm_hw_params_get_rate_max(params, val, dir); 162 + _ = try check(code); 163 + } 164 + 165 + pub inline fn sndPcmHwParamsSetRate(pcm: *SndPcm, params: *SndPcmHwParams, val: c_uint, dir: c_int) !void { 166 + const code = alsa_ffi.snd_pcm_hw_params_set_rate(pcm, params, val, dir); 167 + _ = try check(code); 168 + } 169 + 170 + pub inline fn sndPcmHwParamsSetRateNear(pcm: *SndPcm, params: *SndPcmHwParams, val: *c_uint, dir: *c_int) !void { 171 + const code = alsa_ffi.snd_pcm_hw_params_set_rate_near(pcm, params, val, dir); 172 + _ = try check(code); 173 + } 174 + 175 + pub inline fn sndPcmHwParamsSetPeriodSizeNear( 176 + pcm: *SndPcm, 177 + params: *SndPcmHwParams, 178 + val: *c_ulong, 179 + dir: *c_int, 180 + ) !void { 181 + const code = alsa_ffi.snd_pcm_hw_params_set_period_size_near(pcm, params, val, dir); 182 + _ = try check(code); 183 + } 184 + 185 + pub inline fn sndPcmHwParamsSetBufferSizeNear( 186 + pcm: *SndPcm, 187 + params: *SndPcmHwParams, 188 + val: *c_ulong, 189 + ) !void { 190 + const code = alsa_ffi.snd_pcm_hw_params_set_buffer_size_near(pcm, params, val); 191 + _ = try check(code); 192 + } 193 + 194 + pub inline fn sndPcmHwParams(pcm: *SndPcm, params: *SndPcmHwParams) !void { 195 + const code = alsa_ffi.snd_pcm_hw_params(pcm, params); 196 + _ = try check(code); 197 + } 198 + 199 + pub inline fn sndPcmPrepare(pcm: *SndPcm) !void { 200 + const code = alsa_ffi.snd_pcm_prepare(pcm); 201 + _ = try check(code); 202 + } 203 + 204 + pub inline fn sndPcmWriteI( 205 + pcm: *SndPcm, 206 + buffer_ptr: *const anyopaque, 207 + buffer_len: usize, 208 + ) error{ BadFileDescriptor, BufferUnderrun, StreamSuspended }!usize { 209 + const code = alsa_ffi.snd_pcm_writei(pcm, buffer_ptr, buffer_len); 210 + return check(code) catch |err| @errorCast(err); 211 + } 212 + 213 + fn check(code: anytype) !std.math.IntFittingRange(0, std.math.maxInt(@TypeOf(code))) { 214 + return switch (code) { 215 + 0...std.math.maxInt(@TypeOf(code)) => @intCast(code), 216 + -alsa_ffi.EBUSY => error.DeviceBusy, 217 + -alsa_ffi.ENOENT, -alsa_ffi.ENODEV => error.DeviceNotFound, 218 + -alsa_ffi.EACCES, -alsa_ffi.EPERM => error.AccessDenied, 219 + -alsa_ffi.EBADF => error.BadFileDescriptor, 220 + -alsa_ffi.EPIPE => error.BufferUnderrun, 221 + -alsa_ffi.ESTRPIPE => error.StreamSuspended, 222 + 223 + else => error.Unexpected, 224 + }; 225 + } 226 + 227 + pub inline fn sndStrError(err_num: c_int) [*:0]const u8 { 228 + return alsa_ffi.snd_strerror(err_num); 229 + }
+23 -10
src/main.zig
··· 2 2 const Io = std.Io; 3 3 const Allocator = std.mem.Allocator; 4 4 5 - const midi = @import("midi-synth").midi; 6 - const MidiMessage = midi.protocol.MidiMessage; 5 + const midi_synth = @import("midi-synth"); 6 + const midi = midi_synth.midi; 7 + const synth = midi_synth.synth; 8 + const MidiEvent = midi.protocol.MidiEvent; 9 + const alsa = midi_synth.alsa; 7 10 8 11 pub const std_options: std.Options = .{ 9 12 .fmt_max_depth = 10, ··· 41 44 } 42 45 43 46 if (midi_devs.items.len == 0) { 44 - try stderr.writeAll("No MIDI devices"); 47 + try stderr.writeAll("No MIDI devices\n"); 45 48 try stderr.flush(); 46 49 return; 47 50 } 48 51 52 + try stderr.writeAll("Accessing the device...\n"); 53 + try stderr.flush(); 54 + 49 55 // accessing the first device. 50 56 const midi_dev = try snd_dir.openFile(io, midi_devs.items[0], .{}); 57 + 58 + try stderr.writeAll("Access successful\n"); 59 + try stderr.flush(); 60 + 51 61 var midi_dev_file_reader = midi_dev.reader(io, &buf); 52 62 const midi_dev_reader = &midi_dev_file_reader.interface; 53 63 54 - var midi_buf: [16]MidiMessage = undefined; 55 - var queue: Io.Queue(MidiMessage) = .init(&midi_buf); 64 + var midi_buf: [32]MidiEvent = undefined; 65 + var queue: Io.Queue(MidiEvent) = .init(&midi_buf); 56 66 57 67 const SelectTask = union(enum) { 58 68 streamMidi: @typeInfo(@TypeOf(midi.streamMidi)).@"fn".return_type.?, 59 - logMidi: @typeInfo(@TypeOf(logMidi)).@"fn".return_type.?, 69 + // logMidi: @typeInfo(@TypeOf(logMidi)).@"fn".return_type.?, 70 + playMidi: @typeInfo(@TypeOf(synth.playMidi)).@"fn".return_type.?, 60 71 }; 61 72 var select_buf: [1]SelectTask = undefined; 62 73 var select: Io.Select(SelectTask) = .init(io, &select_buf); 63 74 64 - select.async(.logMidi, logMidi, .{ io, init.gpa, &queue, stderr }); 65 - select.async(.streamMidi, midi.streamMidi, .{ io, init.gpa, midi_dev_reader, &queue }); 75 + // select.async(.logMidi, logMidi, .{ io, init.gpa, &queue, stderr }); 76 + try select.concurrent(.playMidi, synth.playMidi, .{ io, init.gpa, &queue }); 77 + try select.concurrent(.streamMidi, midi.streamMidi, .{ io, init.gpa, midi_dev_reader, &queue }); 66 78 67 79 switch (try select.await()) { 68 - inline .logMidi, .streamMidi => |res| res catch |err| { 80 + inline else => |res| res catch |err| { 81 + queue.close(io); 69 82 select.cancelDiscard(); 70 83 return err; 71 84 }, 72 85 } 73 86 } 74 87 75 - fn logMidi(io: Io, gpa: Allocator, in_queue: *Io.Queue(MidiMessage), w: *Io.Writer) (Io.Cancelable || Io.Writer.Error)!void { 88 + fn logMidi(io: Io, gpa: Allocator, in_queue: *Io.Queue(MidiEvent), w: *Io.Writer) !void { 76 89 while (true) { 77 90 const message = in_queue.getOne(io) catch |err| switch (err) { 78 91 error.Canceled => return error.Canceled,
+13 -12
src/midi.zig
··· 6 6 const unpacked = @import("midi/protocol/unpacked.zig"); 7 7 const @"packed" = @import("midi/protocol/packed.zig"); 8 8 9 - pub const MidiMessage = unpacked.MidiMessage; 9 + pub const MidiEvent = unpacked.MidiEvent; 10 10 pub const Status = unpacked.Status; 11 - pub const StatusPayload = unpacked.StatusPayload; 11 + pub const StatusKind = unpacked.StatusKind; 12 12 pub const SystemMessage = unpacked.SystemMessage; 13 13 pub const SmpteFrame = unpacked.SmpteFrame; 14 14 pub const MtcQuarterFrame = unpacked.MtcQuarterFrame; ··· 20 20 io: Io, 21 21 gpa: Allocator, 22 22 midi_dev_reader: *Io.Reader, 23 - out_queue: *Io.Queue(protocol.MidiMessage), 24 - ) (Allocator.Error || Io.Reader.Error || Io.Cancelable)!void { 23 + out_queue: *Io.Queue(protocol.MidiEvent), 24 + ) !void { 25 25 const MidiByte = protocol.@"packed".MidiByte; 26 26 27 27 var state: StreamState = .idle; ··· 30 30 var mtc_frame_progress: MtcFrameProgress = .none; 31 31 32 32 while (true) { 33 + try io.checkCancel(); 33 34 const byte: MidiByte = @bitCast(try midi_dev_reader.takeByte()); 34 35 switch (byte.kind) { 35 36 .status => switch (handleStatusByte(io, gpa, &state, byte.payload.status, out_queue)) { ··· 59 60 gpa: Allocator, 60 61 state: *StreamState, 61 62 status: protocol.@"packed".StatusByte, 62 - out_queue: *Io.Queue(protocol.MidiMessage), 63 + out_queue: *Io.Queue(protocol.MidiEvent), 63 64 ) ControlFlow(void, void, (Io.Cancelable || Allocator.Error)!void) { 64 65 switch (status.message) { 65 66 .system_message => switch (status.payload.system) { ··· 130 131 data: u7, 131 132 mtc_quarter_frame: protocol.@"packed".MtcQuarterFrame, 132 133 }, 133 - out_queue: *Io.Queue(protocol.MidiMessage), 134 + out_queue: *Io.Queue(protocol.MidiEvent), 134 135 ) ControlFlow(void, void, (Io.Cancelable || Allocator.Error)!void) { 135 136 switch (state.*) { 136 137 .idle => { // missed the start of the message, so no clue where to put the data ··· 180 181 ); 181 182 182 183 const message_tag = @tagName(next_state_tag); 183 - const status_payload = @unionInit( 184 - protocol.StatusPayload, 184 + const status_kind = @unionInit( 185 + protocol.StatusKind, 185 186 message_tag, 186 187 .init(data.last_byte, payload.data), 187 188 ); 188 189 189 190 out_queue.putOne( 190 191 io, 191 - .{ .status = .{ .channel = data.channel, .payload = status_payload } }, 192 + .{ .status = .{ .channel = data.channel, .kind = status_kind } }, 192 193 ) catch |err| switch (err) { 193 194 error.Canceled => return .{ .@"return" = error.Canceled }, 194 195 error.Closed => return .@"break", ··· 198 199 .channel_pressure, 199 200 => |data, tag| { 200 201 const message_tag = @tagName(tag); 201 - const status_payload = @unionInit( 202 - protocol.StatusPayload, 202 + const status_kind = @unionInit( 203 + protocol.StatusKind, 203 204 message_tag, 204 205 .init(payload.data), 205 206 ); 206 207 207 208 out_queue.putOne( 208 209 io, 209 - .{ .status = .{ .channel = data.channel, .payload = status_payload } }, 210 + .{ .status = .{ .channel = data.channel, .kind = status_kind } }, 210 211 ) catch |err| switch (err) { 211 212 error.Canceled => return .{ .@"return" = error.Canceled }, 212 213 error.Closed => return .@"break",
+4 -4
src/midi/protocol/unpacked.zig
··· 5 5 const Framerate = protocol.Framerate; 6 6 const MtcPieceType = protocol.MtcPieceType; 7 7 8 - pub const MidiMessage = union(enum) { 8 + pub const MidiEvent = union(enum) { 9 9 status: Status, 10 10 system: SystemMessage, 11 11 12 - pub fn deinit(self: MidiMessage, gpa: Allocator) void { 12 + pub fn deinit(self: MidiEvent, gpa: Allocator) void { 13 13 switch (self) { 14 14 else => {}, 15 15 .system => |message| message.deinit(gpa), ··· 19 19 20 20 pub const Status = struct { 21 21 channel: u4, 22 - payload: StatusPayload, 22 + kind: StatusKind, 23 23 }; 24 24 25 - pub const StatusPayload = union(enum) { 25 + pub const StatusKind = union(enum) { 26 26 note_off: struct { 27 27 pitch: u7, 28 28 velocity: u7,
+2
src/root.zig
··· 1 + pub const alsa = @import("alsa.zig"); 1 2 pub const midi = @import("midi.zig"); 3 + pub const synth = @import("synth.zig");
+202
src/synth.zig
··· 1 + const std = @import("std"); 2 + const Io = std.Io; 3 + const Allocator = std.mem.Allocator; 4 + 5 + const alsa = @import("alsa.zig"); 6 + const midi = @import("midi.zig"); 7 + const MidiEvent = midi.protocol.MidiEvent; 8 + 9 + /// A4 10 + const reference_pitch = 69; 11 + /// MHz 12 + const reference_freq = 440; 13 + 14 + const note_count = std.math.maxInt(u7); 15 + const notes: [note_count]f64 = blk: { 16 + @setEvalBranchQuota(10026); 17 + 18 + var result: [note_count]f64 = undefined; 19 + for (0..note_count) |i| { 20 + // note_i = note_69 * 2^((i - 69) / 12) 21 + result[i] = reference_freq * std.math.pow(f64, 2, @as(f64, @floatFromInt(@as(i8, i) - reference_pitch)) / 12); 22 + } 23 + break :blk result; 24 + }; 25 + 26 + pub fn playMidi(io: Io, gpa: Allocator, in_queue: *Io.Queue(MidiEvent)) !void { 27 + std.log.debug("Notes: {any}", .{notes}); 28 + 29 + const pcm: *alsa.SndPcm = try alsa.sndPcmOpen("default", .playback, .block); 30 + defer alsa.sndPcmClose(pcm) catch {}; 31 + 32 + const period_size = 512; 33 + const sample_rate = try setupPcm(pcm, period_size); // My sample rate is said to be 384000 Hz 34 + std.log.debug("sample rate: {d}Hz", .{sample_rate}); 35 + 36 + var event_buf: [32]MidiEvent = undefined; 37 + var audio_buf: [period_size]i16 = undefined; 38 + var synth_state: SynthState = .init(sample_rate); 39 + 40 + while (true) { 41 + try io.checkCancel(); 42 + 43 + const events: []const MidiEvent = blk: { 44 + const len = in_queue.get(io, &event_buf, 0) catch |err| switch (err) { 45 + error.Canceled => return error.Canceled, 46 + error.Closed => break, 47 + }; 48 + break :blk event_buf[0..len]; 49 + }; 50 + defer for (events) |event| event.deinit(gpa); 51 + 52 + for (events) |event| { 53 + synth_state.applyEvent(event); 54 + } 55 + 56 + synth_state.renderAudio(&audio_buf); 57 + 58 + var frames_to_write: []const i16 = &audio_buf; 59 + while (frames_to_write.len > 0) { 60 + const frames_written = alsa.sndPcmWriteI(pcm, frames_to_write.ptr, frames_to_write.len) catch |err| switch (err) { 61 + error.BufferUnderrun => { 62 + alsa.sndPcmPrepare(pcm) catch {}; // buffer underrun recovery 63 + break; 64 + }, 65 + else => return error.BadFileDescriptor, 66 + }; 67 + frames_to_write = frames_to_write[frames_written..]; 68 + } 69 + } 70 + } 71 + 72 + /// Returns the native sample rate of the PCM device. 73 + fn setupPcm(pcm: *alsa.SndPcm, period_size: u32) !u32 { 74 + // 1. Allocate & populate with hardware defaults 75 + const params: *alsa.SndPcmHwParams = try alsa.sndPcmHwParamsAlloc(); 76 + defer alsa.sndPcmHwParamsFree(params); 77 + try alsa.sndPcmHwParamsAny(pcm, params); 78 + 79 + // 2. Disable software resampling to expose true hardware capabilities 80 + try alsa.sndPcmHwParamsSetRateResample(pcm, params, false); 81 + 82 + // 3. Set Access Pattern (Interleaved means LRLR...) 83 + try alsa.sndPcmHwParamsSetAccess(pcm, params, .rw_interleaved); 84 + 85 + // 4. Set Format (S16_LE is standard, F32_LE for high precision) 86 + try alsa.sndPcmHwParamsSetFormat(pcm, params, .s16_le); 87 + 88 + // 5. Set Channels (2 for Stereo) 89 + try alsa.sndPcmHwParamsSetChannels(pcm, params, 1); 90 + 91 + // 6. Query the maximum hardware-supported rate 92 + var native_rate: c_uint = undefined; 93 + var dir: c_int = 0; 94 + try alsa.sndPcmHwParamsGetRateMax(params, &native_rate, &dir); 95 + 96 + // 7. Lock ALSA to this specific native rate 97 + try alsa.sndPcmHwParamsSetRate(pcm, params, native_rate, dir); 98 + 99 + // Set period size to match your audio_buf length (512 frames) 100 + var actual_period_size: c_ulong = period_size; 101 + var period_dir: c_int = 0; 102 + try alsa.sndPcmHwParamsSetPeriodSizeNear(pcm, params, &actual_period_size, &period_dir); 103 + 104 + // Set hardware buffer size to 4 periods (2048 frames total) 105 + var buffer_size: c_ulong = actual_period_size * 4; 106 + try alsa.sndPcmHwParamsSetBufferSizeNear(pcm, params, &buffer_size); 107 + 108 + // 8. Apply the parameters to the hardware 109 + try alsa.sndPcmHwParams(pcm, params); 110 + 111 + // 9: Prepare the device for starting 112 + try alsa.sndPcmPrepare(pcm); 113 + 114 + return native_rate; 115 + } 116 + 117 + const Voice = struct { 118 + // active: bool = false, 119 + phase: f64 = 0, 120 + phase_increment: f64 = 0, 121 + /// target volume 122 + velocity: f64 = 0, 123 + /// current, smoothed volume 124 + amplitude: f64 = 0, 125 + }; 126 + 127 + const SynthState = struct { 128 + rate: f64, 129 + voices: [127]Voice = .{Voice{}} ** 127, 130 + 131 + pub fn init(rate: u32) SynthState { 132 + const rate_f: f64 = @floatFromInt(rate); 133 + 134 + return .{ 135 + .rate = rate_f, 136 + }; 137 + } 138 + 139 + fn phaseIncrement(self: *const SynthState, freq: f64) f64 { 140 + return std.math.tau * freq / self.rate; 141 + } 142 + 143 + pub fn applyEvent(self: *SynthState, event: MidiEvent) void { 144 + @setFloatMode(.optimized); 145 + 146 + switch (event) { 147 + else => {}, 148 + .status => |status| switch (status.kind) { 149 + else => {}, 150 + .note_on => |note| if (note.velocity == 0) { 151 + // std.log.debug("note_off: {d}", .{note.pitch}); 152 + self.voices[note.pitch].velocity = 0; 153 + } else { 154 + // std.log.debug("note_on: {d}", .{note.pitch}); 155 + const voice: *Voice = &self.voices[note.pitch]; 156 + 157 + // reset phase for clean attack 158 + voice.phase = 0; 159 + 160 + const freq = notes[note.pitch]; 161 + voice.phase_increment = self.phaseIncrement(freq); 162 + 163 + // Normalize velocity from [1, 127] to [0.0, 1.0] 164 + voice.velocity = @as(f64, @floatFromInt(note.velocity)) / std.math.maxInt(@TypeOf(note.velocity)); 165 + }, 166 + .note_off => |note| { 167 + // std.log.debug("note_off: {d}", .{note.pitch}); 168 + self.voices[note.pitch].velocity = 0; 169 + }, 170 + }, 171 + } 172 + } 173 + 174 + pub fn renderAudio(self: *SynthState, buffer: []i16) void { 175 + @setFloatMode(.optimized); 176 + 177 + for (buffer) |*sample| { 178 + var mix: f64 = 0; 179 + 180 + for (&self.voices) |*voice| if (voice.velocity != 0) { 181 + // Generate sine wave and scale by velocity 182 + mix += std.math.sin(voice.phase) * voice.velocity; 183 + 184 + // Advance the phase 185 + voice.phase += voice.phase_increment; 186 + if (voice.phase >= std.math.tau) { 187 + voice.phase -= std.math.tau; 188 + } 189 + }; 190 + 191 + // Apply headroom reduction (allows ~4 full-velocity notes without any clamping) 192 + const headroom = 4; 193 + var output = mix / headroom; 194 + 195 + // Hard clamp to strictly enforce [-1.0, 1.0] boundaries 196 + output = std.math.clamp(output, -1, 1); 197 + 198 + // Convert normalized float to 16-bit PCM integer 199 + sample.* = @intFromFloat(output * std.math.maxInt(i16)); 200 + } 201 + } 202 + };

History

3 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
feat!: Implement synthesized audio playpack; Rename MidiMessage to MidiEvent
merge conflicts detected
expand
  • build.zig:28
  • src/main.zig:2
  • src/root.zig:1
expand 0 comments
1 commit
expand
feat!: Implement synthesized audio playpack; Rename MidiMessage to MidiEvent
expand 0 comments
1 commit
expand
feat!: Implement audio playpack; Rename MidiMessage to MidiEvent
expand 0 comments