this repo has no description
0
fork

Configure Feed

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

fix: Remove "zipper noise" by smoothing out start and end of a note's wave #3

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/3ml74t37yhd22
+102 -91
Diff #2
+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
··· 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
··· 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
··· 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
sign up or login to add to the discussion
1 commit
expand
fix: Remove "zipper noise" by smoothing out start and end of a note's wave
merge conflicts detected
expand
  • build.zig:28
  • src/main.zig:2
  • src/root.zig:1
expand 0 comments
1 commit
expand
fix: Remove "zipper noise" by smoothing out start and end of a note's wave
expand 0 comments
1 commit
expand
fix: Remove "zipper noise" by smoothing out start and end of a note's wave
expand 0 comments