+485
-26
Diff
round #2
+11
build.zig
+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
src/alsa.h
···
1
+
#include <alsa/asoundlib.h>
+229
src/alsa.zig
+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(¶ms);
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
+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
+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
+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
+2
src/root.zig
+202
src/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
danikvitek.eurosky.social
submitted
#2
1 commit
expand
collapse
feat!: Implement synthesized audio playpack; Rename
MidiMessage to MidiEvent
merge conflicts detected
expand
collapse
expand
collapse
- build.zig:28
- src/main.zig:2
- src/root.zig:1
expand 0 comments
danikvitek.eurosky.social
submitted
#1
1 commit
expand
collapse
feat!: Implement synthesized audio playpack; Rename
MidiMessage to MidiEvent
expand 0 comments
danikvitek.eurosky.social
submitted
#0
1 commit
expand
collapse
feat!: Implement audio playpack; Rename
MidiMessage to MidiEvent