+102
-91
Diff
round #2
+1
-1
build.zig
+1
-1
build.zig
···
26
26
.optimize = optimize,
27
27
.target = target,
28
28
});
29
-
translate_c.linkSystemLibrary("asound", .{ .needed = true }); // change to false when going crossplatform
29
+
translate_c.linkSystemLibrary("asound", .{}); // change to false when going crossplatform
30
30
31
31
const alsa_mod = translate_c.createModule();
32
32
+5
-33
src/main.zig
+5
-33
src/main.zig
···
16
16
const arena: Allocator = init.arena.allocator();
17
17
const io = init.io;
18
18
19
-
var buf: [256]u8 = undefined;
20
-
var stderr_writer = Io.File.stderr().writer(io, &buf);
21
-
const stderr = &stderr_writer.interface;
22
-
23
19
var midi_devs: std.ArrayList([]const u8) = .empty;
24
-
defer {
25
-
for (midi_devs.items) |item| {
26
-
arena.free(item);
27
-
}
28
-
midi_devs.deinit(arena);
29
-
}
30
20
31
21
const snd_dir = try Io.Dir.openDirAbsolute(io, "/dev/snd", .{ .iterate = true });
32
22
defer snd_dir.close(io);
···
37
27
// or std.mem.startsWith(u8, entry.name, "ump")
38
28
)) {
39
29
try midi_devs.append(arena, try arena.dupe(u8, entry.name));
40
-
try stderr.print("/dev/snd/{s}: {t}\n", .{ entry.name, entry.kind });
41
-
try stderr.flush();
30
+
std.log.info("/dev/snd/{s}: {t}", .{ entry.name, entry.kind });
42
31
}
43
32
}
44
33
}
45
34
46
35
if (midi_devs.items.len == 0) {
47
-
try stderr.writeAll("No MIDI devices\n");
48
-
try stderr.flush();
36
+
std.log.info("No MIDI devices", .{});
49
37
return;
50
38
}
51
39
52
-
try stderr.writeAll("Accessing the device...\n");
53
-
try stderr.flush();
40
+
std.log.info("Accessing the device...", .{});
54
41
55
42
// accessing the first device.
56
43
const midi_dev = try snd_dir.openFile(io, midi_devs.items[0], .{});
57
44
58
-
try stderr.writeAll("Access successful\n");
59
-
try stderr.flush();
45
+
std.log.info("Access successful", .{});
60
46
47
+
var buf: [512]u8 = undefined;
61
48
var midi_dev_file_reader = midi_dev.reader(io, &buf);
62
49
const midi_dev_reader = &midi_dev_file_reader.interface;
63
50
···
66
53
67
54
const SelectTask = union(enum) {
68
55
streamMidi: @typeInfo(@TypeOf(midi.streamMidi)).@"fn".return_type.?,
69
-
// logMidi: @typeInfo(@TypeOf(logMidi)).@"fn".return_type.?,
70
56
playMidi: @typeInfo(@TypeOf(synth.playMidi)).@"fn".return_type.?,
71
57
};
72
58
var select_buf: [1]SelectTask = undefined;
73
59
var select: Io.Select(SelectTask) = .init(io, &select_buf);
74
60
75
-
// select.async(.logMidi, logMidi, .{ io, init.gpa, &queue, stderr });
76
61
try select.concurrent(.playMidi, synth.playMidi, .{ io, init.gpa, &queue });
77
62
try select.concurrent(.streamMidi, midi.streamMidi, .{ io, init.gpa, midi_dev_reader, &queue });
78
63
···
84
69
},
85
70
}
86
71
}
87
-
88
-
fn logMidi(io: Io, gpa: Allocator, in_queue: *Io.Queue(MidiEvent), w: *Io.Writer) !void {
89
-
while (true) {
90
-
const message = in_queue.getOne(io) catch |err| switch (err) {
91
-
error.Canceled => return error.Canceled,
92
-
error.Closed => break,
93
-
};
94
-
defer message.deinit(gpa);
95
-
96
-
try w.print("{any}\n", .{message});
97
-
try w.flush();
98
-
}
99
-
}
+43
-31
src/midi.zig
+43
-31
src/midi.zig
···
79
79
};
80
80
errdefer gpa.free(message);
81
81
82
+
const event: protocol.MidiEvent = .{ .system = .{ .system_exclusive = message } };
83
+
std.log.debug("{any}", .{event});
84
+
82
85
state.* = .idle;
83
-
out_queue.putOne(
84
-
io,
85
-
.{ .system = .{ .system_exclusive = message } },
86
-
) catch |err| switch (err) {
86
+
out_queue.putOne(io, event) catch |err| switch (err) {
87
87
error.Canceled => return .{ .@"return" = error.Canceled },
88
88
error.Closed => return .@"break",
89
89
};
···
97
97
.stop,
98
98
.active_sensing,
99
99
.system_reset,
100
-
=> |tag| out_queue.putOne(io, .{ .system = @unionInit(
101
-
protocol.SystemMessage,
102
-
@tagName(tag),
103
-
{},
104
-
) }) catch |err| switch (err) {
105
-
error.Canceled => return .{ .@"return" = error.Canceled },
106
-
error.Closed => return .@"break",
100
+
=> |tag| {
101
+
const event: protocol.MidiEvent = .{ .system = @unionInit(
102
+
protocol.SystemMessage,
103
+
@tagName(tag),
104
+
{},
105
+
) };
106
+
std.log.debug("{any}", .{event});
107
+
108
+
out_queue.putOne(io, event) catch |err| switch (err) {
109
+
error.Canceled => return .{ .@"return" = error.Canceled },
110
+
error.Closed => return .@"break",
111
+
};
107
112
},
108
113
},
109
114
inline .note_off,
···
180
185
.{ .channel = data.channel },
181
186
);
182
187
183
-
const message_tag = @tagName(next_state_tag);
188
+
const status_tag = @tagName(next_state_tag);
184
189
const status_kind = @unionInit(
185
190
protocol.StatusKind,
186
-
message_tag,
191
+
status_tag,
187
192
.init(data.last_byte, payload.data),
188
193
);
189
194
190
-
out_queue.putOne(
191
-
io,
192
-
.{ .status = .{ .channel = data.channel, .kind = status_kind } },
193
-
) catch |err| switch (err) {
195
+
const event: protocol.MidiEvent = .{ .status = .{ .channel = data.channel, .kind = status_kind } };
196
+
std.log.debug("{any}", .{event});
197
+
198
+
out_queue.putOne(io, event) catch |err| switch (err) {
194
199
error.Canceled => return .{ .@"return" = error.Canceled },
195
200
error.Closed => return .@"break",
196
201
};
···
205
210
.init(payload.data),
206
211
);
207
212
208
-
out_queue.putOne(
209
-
io,
210
-
.{ .status = .{ .channel = data.channel, .kind = status_kind } },
211
-
) catch |err| switch (err) {
213
+
const event: protocol.MidiEvent = .{ .status = .{ .channel = data.channel, .kind = status_kind } };
214
+
std.log.debug("{any}", .{event});
215
+
216
+
out_queue.putOne(io, event) catch |err| switch (err) {
212
217
error.Canceled => return .{ .@"return" = error.Canceled },
213
218
error.Closed => return .@"break",
214
219
};
···
235
240
} },
236
241
};
237
242
if (mtc_frame_progress.add(unpacked_frame)) |smpte_frame| {
238
-
out_queue.putOne(io, .{ .system = .{
239
-
.smpte_frame = smpte_frame,
240
-
} }) catch |err| switch (err) {
243
+
const event: protocol.MidiEvent = .{ .system = .{ .smpte_frame = smpte_frame } };
244
+
std.log.debug("{any}", .{event});
245
+
246
+
out_queue.putOne(io, event) catch |err| switch (err) {
241
247
error.Canceled => return .{ .@"return" = error.Canceled },
242
248
error.Closed => return .@"break",
243
249
};
···
248
254
},
249
255
.song_position_pointer_lsb => |data| {
250
256
state.* = .idle;
251
-
out_queue.putOne(io, .{
252
-
.system = .{
253
-
.song_position_pointer = .init(data.lsb, payload.data),
254
-
},
255
-
}) catch |err| switch (err) {
257
+
258
+
const event: protocol.MidiEvent = .{ .system = .{
259
+
.song_position_pointer = .init(data.lsb, payload.data),
260
+
} };
261
+
std.log.debug("{any}", .{event});
262
+
263
+
out_queue.putOne(io, event) catch |err| switch (err) {
256
264
error.Canceled => return .{ .@"return" = error.Canceled },
257
265
error.Closed => return .@"break",
258
266
};
259
267
},
260
268
.song_select => {
261
269
state.* = .idle;
262
-
out_queue.putOne(io, .{ .system = .{
270
+
271
+
const event: protocol.MidiEvent = .{ .system = .{
263
272
.song_select = .{ .song_number = payload.data },
264
-
} }) catch |err| switch (err) {
273
+
} };
274
+
std.log.debug("{any}", .{event});
275
+
276
+
out_queue.putOne(io, event) catch |err| switch (err) {
265
277
error.Canceled => return .{ .@"return" = error.Canceled },
266
278
error.Closed => return .@"break",
267
279
};
+53
-26
src/synth.zig
+53
-26
src/synth.zig
···
11
11
/// MHz
12
12
const reference_freq = 440;
13
13
14
-
const note_count = std.math.maxInt(u7);
14
+
const note_count = std.math.maxInt(u7) + 1;
15
15
const notes: [note_count]f64 = blk: {
16
-
@setEvalBranchQuota(10026);
16
+
@setEvalBranchQuota(11262);
17
17
18
18
var result: [note_count]f64 = undefined;
19
19
for (0..note_count) |i| {
20
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);
21
+
result[i] = reference_freq * std.math.pow(
22
+
f64,
23
+
2,
24
+
@as(f64, @floatFromInt(@as(std.math.IntFittingRange(-69, note_count), i) - reference_pitch)) / 12,
25
+
);
22
26
}
23
27
break :blk result;
24
28
};
25
29
30
+
// 5ms to reach full volume
31
+
const attack_time_sec: f64 = 0.005;
32
+
// 50ms to fade to silence after note off
33
+
const release_time_sec: f64 = 0.050;
34
+
26
35
pub fn playMidi(io: Io, gpa: Allocator, in_queue: *Io.Queue(MidiEvent)) !void {
27
36
std.log.debug("Notes: {any}", .{notes});
28
37
···
31
40
32
41
const period_size = 512;
33
42
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});
43
+
std.log.info("sample rate: {d}Hz", .{sample_rate});
35
44
36
45
var event_buf: [32]MidiEvent = undefined;
37
46
var audio_buf: [period_size]i16 = undefined;
···
115
124
}
116
125
117
126
const Voice = struct {
118
-
// active: bool = false,
119
127
phase: f64 = 0,
120
128
phase_increment: f64 = 0,
121
-
/// target volume
122
-
velocity: f64 = 0,
123
129
/// current, smoothed volume
124
-
amplitude: f64 = 0,
130
+
current_volume: f64 = 0,
131
+
target_volume: f64 = 0,
125
132
};
126
133
127
134
const SynthState = struct {
128
135
rate: f64,
129
-
voices: [127]Voice = .{Voice{}} ** 127,
136
+
voices: [note_count]Voice = .{Voice{}} ** note_count,
137
+
138
+
// Envelope speeds (amount to change amplitude per sample)
139
+
attack_increment: f64,
140
+
release_decrement: f64,
130
141
131
142
pub fn init(rate: u32) SynthState {
132
143
const rate_f: f64 = @floatFromInt(rate);
133
144
134
145
return .{
135
146
.rate = rate_f,
147
+
.attack_increment = 1.0 / (attack_time_sec * rate_f),
148
+
.release_decrement = 1.0 / (release_time_sec * rate_f),
136
149
};
137
150
}
138
151
···
148
161
.status => |status| switch (status.kind) {
149
162
else => {},
150
163
.note_on => |note| if (note.velocity == 0) {
151
-
// std.log.debug("note_off: {d}", .{note.pitch});
152
-
self.voices[note.pitch].velocity = 0;
164
+
self.voices[note.pitch].target_volume = 0;
153
165
} else {
154
-
// std.log.debug("note_on: {d}", .{note.pitch});
155
166
const voice: *Voice = &self.voices[note.pitch];
156
167
157
-
// reset phase for clean attack
158
-
voice.phase = 0;
168
+
// Only reset phase if the note was fully silent
169
+
if (voice.current_volume == 0) {
170
+
voice.phase = 0;
171
+
}
159
172
160
173
const freq = notes[note.pitch];
161
174
voice.phase_increment = self.phaseIncrement(freq);
162
175
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));
176
+
// Set the target volume. Normalize velocity from [1, 127] to (0.0, 1.0]
177
+
voice.target_volume = @as(f64, @floatFromInt(note.velocity)) / std.math.maxInt(@TypeOf(note.velocity));
165
178
},
166
179
.note_off => |note| {
167
-
// std.log.debug("note_off: {d}", .{note.pitch});
168
-
self.voices[note.pitch].velocity = 0;
180
+
self.voices[note.pitch].target_volume = 0;
169
181
},
170
182
},
171
183
}
···
177
189
for (buffer) |*sample| {
178
190
var mix: f64 = 0;
179
191
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;
192
+
for (&self.voices) |*voice| {
193
+
// 1. Step the amplitude towards the target velocity
194
+
if (voice.current_volume < voice.target_volume) {
195
+
voice.current_volume = @min(
196
+
voice.current_volume + self.attack_increment,
197
+
voice.target_volume,
198
+
);
199
+
} else if (voice.current_volume > voice.target_volume) {
200
+
voice.current_volume = @max(
201
+
voice.current_volume - self.release_decrement,
202
+
voice.target_volume,
203
+
);
204
+
}
183
205
184
-
// Advance the phase
185
-
voice.phase += voice.phase_increment;
186
-
if (voice.phase >= std.math.tau) {
187
-
voice.phase -= std.math.tau;
206
+
if (voice.current_volume > 0) {
207
+
// Generate sine wave and scale by the smoothed amplitude
208
+
mix += std.math.sin(voice.phase) * voice.current_volume;
209
+
210
+
// Advance the phase
211
+
voice.phase += voice.phase_increment;
212
+
if (voice.phase >= std.math.tau) {
213
+
voice.phase -= std.math.tau;
214
+
}
188
215
}
189
-
};
216
+
}
190
217
191
218
// Apply headroom reduction (allows ~4 full-velocity notes without any clamping)
192
219
const headroom = 4;
History
3 rounds
0 comments
danikvitek.eurosky.social
submitted
#2
1 commit
expand
collapse
fix: Remove "zipper noise" by smoothing out start and end of a note's wave
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
fix: Remove "zipper noise" by smoothing out start and end of a note's wave
expand 0 comments
danikvitek.eurosky.social
submitted
#0
1 commit
expand
collapse
fix: Remove "zipper noise" by smoothing out start and end of a note's wave