diff --git a/src/root.zig b/src/root.zig index 3c7a5a7..05c764c 100644 --- a/src/root.zig +++ b/src/root.zig @@ -13,8 +13,5 @@ test { // _ = @import("theory_v1/note.zig"); // _ = @import("theory_v1/pitch.zig"); // _ = @import("theory_v1/scale.zig"); - _ = @import("theory_v2/interval.zig"); - _ = @import("theory_v2/note.zig"); - _ = @import("theory_v2/pitch.zig"); - _ = @import("theory_v2/scale.zig"); + _ = @import("theory/note.zig"); } diff --git a/src/theory/constants.zig b/src/theory/constants.zig new file mode 100644 index 0000000..8ddd4fd --- /dev/null +++ b/src/theory/constants.zig @@ -0,0 +1,8 @@ +/// The maximum valid MIDI note number. +pub const midi_max = 127; + +/// The number of notes per octave. +pub const notes_per_oct = 7; + +/// The number of semitones per octave. +pub const semis_per_oct = 12; diff --git a/src/theory/note.zig b/src/theory/note.zig new file mode 100644 index 0000000..9c44c50 --- /dev/null +++ b/src/theory/note.zig @@ -0,0 +1,143 @@ +const std = @import("std"); +const testing = std.testing; + +const c = @import("constants.zig"); + +pub const Note = struct { + midi: u7, + + pub const Letter = enum { c, d, e, f, g, a, b }; + pub const Accidental = enum { double_flat, flat, natural, sharp, double_sharp }; + + pub fn init(let: Letter, acc: Accidental, oct: i8) !Note { + const base: i16 = baseSemitones(let); + const offset: i4 = switch (acc) { + .double_flat => -2, + .flat => -1, + .natural => 0, + .sharp => 1, + .double_sharp => 2, + }; + const midi = base + offset + (oct + 1) * c.semis_per_oct; + if (midi < 0 or c.midi_max < midi) { + return error.NoteOutOfRange; + } + return .{ .midi = @intCast(midi) }; + } + + fn baseSemitones(let: Letter) u4 { + return switch (let) { + .c => 0, + .d => 2, + .e => 4, + .f => 5, + .g => 7, + .a => 9, + .b => 11, + }; + } + + pub fn letter(self: Note) Letter { + const pc = self.pitchClass(); + return switch (pc) { + 0, 1 => Letter.c, + 2, 3 => Letter.d, + 4 => Letter.e, + 5, 6 => Letter.f, + 7, 8 => Letter.g, + 9, 10 => Letter.a, + 11 => Letter.b, + else => unreachable, + }; + } + + pub fn accidental(self: Note) Accidental { + const pc: i8 = self.pitchClass(); + const let = self.letter(); + const base = baseSemitones(let); + const diff = @mod(pc - base + c.semis_per_oct, c.semis_per_oct); + return switch (diff) { + 10 => .double_flat, + 11 => .flat, + 0 => .natural, + 1 => .sharp, + 2 => .double_sharp, + else => unreachable, + }; + } + + pub fn octave(self: Note) i8 { + return @divFloor(@as(i8, self.midi), c.semis_per_oct) - 1; + } + + pub fn pitchClass(self: Note) u4 { + return @intCast(@mod(self.midi, c.semis_per_oct)); + } + + pub fn format( + self: Note, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, + ) !void { + _ = fmt; + _ = options; + const let = self.letter(); + const acc = self.accidental(); + const oct = self.octave(); + try writer.print("{c}{s}{d}", .{ + std.ascii.toUpper(@tagName(let)[0]), + switch (acc) { + .double_flat => "𝄫", + .flat => "♭", + .natural => "", + .sharp => "♯", + .double_sharp => "𝄪", + }, + oct, + }); + } +}; + +test "Note initialization" { + try testing.expectError(error.NoteOutOfRange, Note.init(.c, .flat, -1)); + try testing.expectEqual(0, (try Note.init(.c, .natural, -1)).midi); + try testing.expectEqual(58, (try Note.init(.c, .double_flat, 4)).midi); + try testing.expectEqual(59, (try Note.init(.c, .flat, 4)).midi); + try testing.expectEqual(60, (try Note.init(.c, .natural, 4)).midi); + try testing.expectEqual(61, (try Note.init(.c, .sharp, 4)).midi); + try testing.expectEqual(62, (try Note.init(.c, .double_sharp, 4)).midi); + try testing.expectEqual(69, (try Note.init(.a, .natural, 4)).midi); + try testing.expectEqual(127, (try Note.init(.g, .natural, 9)).midi); + try testing.expectError(error.NoteOutOfRange, Note.init(.g, .sharp, 9)); +} + +test "Note properties" { + const c4 = try Note.init(.c, .natural, 4); + try testing.expectEqual(Note.Letter.c, c4.letter()); + try testing.expectEqual(Note.Accidental.natural, c4.accidental()); + try testing.expectEqual(4, c4.octave()); + try testing.expectEqual(0, c4.pitchClass()); + + const cs4 = try Note.init(.c, .sharp, 4); + try testing.expectEqual(Note.Letter.c, cs4.letter()); + try testing.expectEqual(Note.Accidental.sharp, cs4.accidental()); + try testing.expectEqual(4, cs4.octave()); + try testing.expectEqual(1, cs4.pitchClass()); + + // There's currently no naming persistence. + const df4 = try Note.init(.d, .flat, 4); + try testing.expectEqual(Note.Letter.c, df4.letter()); + try testing.expectEqual(Note.Accidental.sharp, df4.accidental()); + try testing.expectEqual(4, df4.octave()); + try testing.expectEqual(1, df4.pitchClass()); +} + +test "Note formatting" { + // There's currently no naming persistence. + try testing.expectFmt("A♯3", "{}", .{try Note.init(.c, .double_flat, 4)}); + try testing.expectFmt("B3", "{}", .{try Note.init(.c, .flat, 4)}); + try testing.expectFmt("C4", "{}", .{try Note.init(.c, .natural, 4)}); + try testing.expectFmt("C♯4", "{}", .{try Note.init(.c, .sharp, 4)}); + try testing.expectFmt("D4", "{}", .{try Note.init(.c, .double_sharp, 4)}); +}