Skip to content

Commit

Permalink
feat: new note module based on storing only the midi number
Browse files Browse the repository at this point in the history
  • Loading branch information
elasticdog committed Aug 27, 2024
1 parent 8c61159 commit 942ece9
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 4 deletions.
5 changes: 1 addition & 4 deletions src/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
8 changes: 8 additions & 0 deletions src/theory/constants.zig
Original file line number Diff line number Diff line change
@@ -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;
143 changes: 143 additions & 0 deletions src/theory/note.zig
Original file line number Diff line number Diff line change
@@ -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)});
}

0 comments on commit 942ece9

Please sign in to comment.