Skip to content

Commit

Permalink
feat: get MIDI input working to label pressed keys
Browse files Browse the repository at this point in the history
This needs refactoring, but again, it's working!
  • Loading branch information
elasticdog committed Sep 2, 2024
1 parent 960274b commit 19aadda
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 33 deletions.
5 changes: 5 additions & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub fn main() !void {
var app = try Application.init(padding);
const app_name = "ClefCraft";

try app.setupMidiInput();

var midi_output = try MidiOutput.init(app_name);
defer midi_output.deinit();

Expand All @@ -29,6 +31,9 @@ pub fn main() !void {
rl.initWindow(window_width, window_height, app_name);
defer rl.closeWindow();

// // Set the default font size for raygui.
// rg.guiSetStyle(rg.GuiControl.default, @intFromEnum(rg.GuiDefaultProperty.text_size), 20);

rl.setTargetFPS(60);

while (!rl.windowShouldClose()) {
Expand Down
35 changes: 35 additions & 0 deletions src/midi/input.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const std = @import("std");
const rtmidi = @import("rtmidi");

pub const MidiInput = struct {
midi_in: *rtmidi.In,
callback_fn: ?*const fn (f64, []const u8, ?*anyopaque) void,

pub fn init() !MidiInput {
var midi_in = rtmidi.In.createDefault() orelse return error.MidiInCreateFailed;
errdefer midi_in.destroy();

return MidiInput{
.midi_in = midi_in,
.callback_fn = null,
};
}

pub fn deinit(self: *MidiInput) void {
self.midi_in.destroy();
}

pub fn openPort(self: *MidiInput, port: usize, port_name: [:0]const u8) !void {
self.midi_in.openPort(port, port_name);
}

pub fn setCallback(self: *MidiInput, comptime callback: fn (f64, []const u8, ?*anyopaque) void, user_data: ?*anyopaque) void {
self.callback_fn = callback;
self.midi_in.setCallback(callback, user_data);
}

pub fn cancelCallback(self: *MidiInput) void {
self.midi_in.cancelCallback();
self.callback_fn = null;
}
};
1 change: 1 addition & 0 deletions src/midi/output.zig
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const std = @import("std");
const rtmidi = @import("rtmidi");

pub const MidiOutput = struct {
Expand Down
44 changes: 43 additions & 1 deletion src/ui/application.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const std = @import("std");
const log = std.log.scoped(.application);

const Coord = @import("coord.zig").Coord;
const MidiInput = @import("../midi/input.zig").MidiInput;
const MidiOutput = @import("../midi/output.zig").MidiOutput;
const Mouse = @import("mouse.zig").Mouse;
const Note = @import("../theory/note.zig").Note;
Expand All @@ -12,27 +14,67 @@ pub const Application = struct {
piano: Piano,
tonality: Tonality,
tonality_selector: TonalitySelector,
midi_input: MidiInput,

pub fn init(padding: i32) !Application {
const piano = try Piano.init(Coord{ .x = padding, .y = 156 });
const tonality = Tonality.init(try Note.fromString("C4"), .major);
const tonality_selector = TonalitySelector.init(Coord{ .x = padding, .y = 16 }, tonality);
const midi_input = try MidiInput.init();

return .{
.piano = piano,
.tonality = tonality,
.tonality_selector = tonality_selector,
.midi_input = midi_input,
};
}

pub fn deinit(self: *Application) void {
self.midi_input.deinit();
}

pub fn update(self: *Application, mouse: Mouse, midi_output: *MidiOutput) !void {
self.tonality_selector.update(mouse);
self.tonality = self.tonality_selector.selected_tonality;
try self.piano.update(mouse, midi_output);
}

pub fn draw(self: Application) void {
pub fn draw(self: *const Application) void {
self.tonality_selector.draw();
self.piano.draw(self.tonality);
}

pub fn setupMidiInput(self: *Application) !void {
try self.midi_input.openPort(1, "ClefCraft Input");
self.midi_input.setCallback(midiCallback, self);
log.debug("MIDI input setup completed", .{});
}

fn midiCallback(timestamp: f64, message: []const u8, user_data: ?*anyopaque) void {
_ = timestamp;
const self: *Application = @ptrCast(@alignCast(user_data.?));

if (message.len >= 3) {
const status = message[0];
const note: u7 = @truncate(message[1]);
const velocity = message[2];

log.debug("MIDI message: status={x:0>2}, note={}, velocity={}", .{ status, note, velocity });

switch (status & 0xF0) {
0x90 => { // Note On
self.piano.setKeyState(note, velocity > 0);
},
0x80 => { // Note Off
self.piano.setKeyState(note, false);
},
else => {
log.debug("Unhandled MIDI status: 0x{x:0>2}", .{status});
},
}
} else {
log.debug("Received incomplete MIDI message", .{});
}
}
};
67 changes: 35 additions & 32 deletions src/ui/piano.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const key_height_white = 160;
pub const Piano = struct {
keys: [key_count]Key,
pos: Coord,
midi_key_states: [128]bool,

pub fn init(pos: Coord) !Piano {
var keys = [_]Key{.{}} ** key_count;
Expand All @@ -31,7 +32,11 @@ pub const Piano = struct {
key.height = if (key.is_black) key_height_black else key_height_white;
}

return Piano{ .keys = keys, .pos = pos };
return Piano{
.keys = keys,
.pos = pos,
.midi_key_states = [_]bool{false} ** 128,
};
}

pub fn width(_: Piano) i32 {
Expand Down Expand Up @@ -68,59 +73,51 @@ pub const Piano = struct {

// Update all of the key states.
for (&self.keys) |*key| {
const midi_pressed = self.midi_key_states[key.midi_number];
const mouse_pressed = (key == focused_key and mouse.is_pressed_left);

switch (key.state) {
.disabled => {
continue;
},
.disabled => continue,
.normal => {
if (key == focused_key) {
if (midi_pressed or mouse_pressed) {
key.state = .pressed;
} else if (key == focused_key) {
key.state = .focused;
}
},
.focused => {
if (key != focused_key) {
key.state = .normal;
} else if (mouse.is_pressed_left) {
if (midi_pressed or mouse_pressed) {
key.state = .pressed;
} else if (key != focused_key) {
key.state = .normal;
}
},
.pressed => {
if (!mouse.is_pressed_left) {
if (!midi_pressed and !mouse_pressed) {
key.state = if (key == focused_key) .focused else .normal;
}
},
}

// Handle MIDI events.
switch (key.state) {
.disabled => {
continue;
},
.pressed => {
if (key.state_prev != .pressed) {
log.debug("sending message note on for: {}", .{key.midi_number});
try midi_output.noteOn(1, key.midi_number, 112);
}
},
.focused => {
if (key.state_prev == .pressed) {
log.debug("sending message note off for: {}", .{key.midi_number});
try midi_output.noteOff(1, key.midi_number, 0);
}
},
.normal => {
if (key.state_prev == .pressed) {
log.debug("sending message note off for: {}", .{key.midi_number});
try midi_output.noteOff(1, key.midi_number, 0);
}
},
// Handle MIDI output events.
if (key.state == .pressed and key.state_prev != .pressed) {
log.debug("sending message note on for: {}", .{key.midi_number});
try midi_output.noteOn(1, key.midi_number, 112);
} else if (key.state != .pressed and key.state_prev == .pressed) {
log.debug("sending message note off for: {}", .{key.midi_number});
try midi_output.noteOff(1, key.midi_number, 0);
}

// Update the previous key state.
key.state_prev = key.state;
}
}

pub fn setKeyState(self: *Piano, midi_number: u7, is_pressed: bool) void {
self.midi_key_states[midi_number] = is_pressed;
log.debug("MIDI key state changed: number={}, pressed={}", .{ midi_number, is_pressed });
}

pub fn draw(self: *const Piano, tonality: Tonality) void {
// Draw the white keys first, then the black keys on top.
for (self.keys) |key| if (!key.is_black) key.draw(tonality);
Expand Down Expand Up @@ -161,6 +158,12 @@ const Key = struct {
disabled,
};

pub fn setState(self: *Key, new_state: State) void {
log.debug("Key state change: MIDI number={}, old state={}, new state={}", .{ self.midi_number, self.state, new_state });
self.state_prev = self.state;
self.state = new_state;
}

fn color(self: Key) rl.Color {
return switch (self.state) {
.normal => if (self.is_black) rl.Color.black else rl.Color.white,
Expand Down

0 comments on commit 19aadda

Please sign in to comment.