Skip to content

Commit

Permalink
feat: implement the application of intervals to notes
Browse files Browse the repository at this point in the history
  • Loading branch information
elasticdog committed Aug 30, 2024
1 parent 9d8bcd7 commit 074c439
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 27 deletions.
72 changes: 55 additions & 17 deletions src/theory/interval.zig
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,32 @@ pub const Interval = struct {
return .{ .qual = qual, .num = num };
}

// pub fn applyTo(self: Interval, note: Note) Note {}
/// Applies the interval to a given note, returning the resulting note.
pub fn applyTo(self: Interval, note: Note) !Note {
const new_midi = @as(i16, note.midi) + self.semitones();

const let_index = @intFromEnum(note.name.let);
const new_let_index = @mod(let_index + self.num - 1, c.notes_per_oct);
const new_let: Note.Letter = @enumFromInt(new_let_index);

const expected_pc = new_let.semitones();
const actual_pc = @mod(new_midi, c.semis_per_oct);

const acc_diff = actual_pc - expected_pc;
const new_acc: Note.Accidental = switch (acc_diff) {
-2 => .double_flat,
-1 => .flat,
0 => .natural,
1 => .sharp,
2 => .double_sharp,
else => return error.InvalidInterval,
};

const new_oct: i8 = @intCast(@divFloor(new_midi, c.semis_per_oct) - 1);
std.debug.print("self: {}, note: {}, new_let: {s}, new_acc: {s}, new_oct: {}\n", .{ self, note, @tagName(new_let), @tagName(new_acc), new_oct });

return Note.init(new_let, new_acc, new_oct);
}

/// Returns the inversion of the interval.
pub fn invert(self: Interval) Interval {
Expand Down Expand Up @@ -98,34 +123,24 @@ pub const Interval = struct {

/// Returns the number of semitones in the interval.
pub fn semitones(self: Interval) u7 {
const base = baseSemitones(self.num);
const base_sem = baseSemitones(self.num);

const offset: i8 = switch (self.qual) {
const qual_offset: i8 = switch (self.qual) {
.perfect, .major => 0,
.minor => -1,
.augmented => 1,
.diminished => if (isPerfect(self.num)) -1 else -2,
};

return @intCast(base + offset);
return @intCast(base_sem + qual_offset);
}

fn baseSemitones(num: u7) i8 {
const simplified = @mod(num - 1, c.notes_per_oct) + 1;
const simple_inter = @mod(num - 1, c.notes_per_oct) + 1;
const base_sem = @as(Note.Letter, @enumFromInt(simple_inter - 1)).semitones();
const oct_offset = (num - 1) / c.notes_per_oct * c.semis_per_oct;

const base: i8 = switch (simplified) {
1 => 0, // Unison
2 => 2, // Second
3 => 4, // Third
4 => 5, // Fourth
5 => 7, // Fifth
6 => 9, // Sixth
7 => 11, // Seventh
else => unreachable,
};

return @intCast(base + oct_offset);
return @intCast(base_sem + oct_offset);
}

/// Checks if the interval is compound (larger than an octave).
Expand Down Expand Up @@ -360,6 +375,29 @@ test "calculation between enharmonic notes" {
try testing.expectEqual(Interval.d4, Interval.between(d4, gf4));
}

test "application to notes" {
const c4 = try Note.fromString("C4");
const e4 = try Note.fromString("E4");
const g4 = try Note.fromString("G4");
const c5 = try Note.fromString("C5");

try testing.expectEqual(e4, try Interval.M3.applyTo(c4));
try testing.expectEqual(g4, try Interval.P5.applyTo(c4));
try testing.expectEqual(c5, try Interval.P8.applyTo(c4));

const fs4 = try Note.fromString("F#4");
const as4 = try Note.fromString("A#4");
try testing.expectEqual(as4, try Interval.M3.applyTo(fs4));

const bf3 = try Note.fromString("Bb3");
const af4 = try Note.fromString("Ab4");
try testing.expectEqual(af4, try Interval.m7.applyTo(bf3));

const e5 = try Note.fromString("E5");
const cx6 = try Note.fromString("Cx6");
try testing.expectEqual(cx6, try Interval.A6.applyTo(e5));
}

test "simple inversions" {
try testing.expectEqual(Interval.P8, Interval.P1.invert());
try testing.expectEqual(Interval.m3, Interval.M6.invert());
Expand Down
34 changes: 24 additions & 10 deletions src/theory/note.zig
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,35 @@ pub const Note = struct {
acc: Accidental,
};

pub const Letter = enum { c, d, e, f, g, a, b };
pub const Letter = enum {
c,
d,
e,
f,
g,
a,
b,

/// Converts a Letter to its corresponding semitone value.
pub fn semitones(self: Letter) u4 {
return switch (self) {
.c => 0,
.d => 2,
.e => 4,
.f => 5,
.g => 7,
.a => 9,
.b => 11,
};
}
};

pub const Accidental = enum { double_flat, flat, natural, sharp, double_sharp };

/// Creates a note from a letter, accidental, and octave.
/// Returns an error if the resulting note is out of the valid MIDI range.
pub fn init(let: Letter, acc: Accidental, oct: i8) !Note {
const base: i16 = switch (let) {
.c => 0,
.d => 2,
.e => 4,
.f => 5,
.g => 7,
.a => 9,
.b => 11,
};
const base: i16 = let.semitones();
const offset: i16 = switch (acc) {
.double_flat => -2,
.flat => -1,
Expand Down

0 comments on commit 074c439

Please sign in to comment.