Skip to content

Commit

Permalink
feat: add initial scale implementation
Browse files Browse the repository at this point in the history
This still needs a bit of tweaking, but I think we've arrived a similar,
but better structure compared to the theory.old setup.
  • Loading branch information
elasticdog committed Aug 24, 2024
1 parent 3ab4418 commit 0a660f4
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 1 deletion.
144 changes: 143 additions & 1 deletion src/theory/scale.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,147 @@ const log = std.log.scoped(.scale);
const testing = std.testing;

const Interval = @import("interval.zig").Interval;
const Note = @import("note.zig").Note;
const Pattern = @import("scale_library.zig").Pattern;
const Pitch = @import("pitch.zig").Pitch;

pub const Scale = struct {};
pub const Scale = struct {
tonic: Note,
pattern: Pattern,
allocator: std.mem.Allocator,
intervals_cache: ?[]const Interval,
notes_cache: ?[]Note,

pub fn init(
allocator: std.mem.Allocator,
tonic: Note,
pattern: Pattern,
) Scale {
return .{
.tonic = tonic,
.pattern = pattern,
.allocator = allocator,
.intervals_cache = null,
.notes_cache = null,
};
}

pub fn deinit(self: *Scale) void {
if (self.intervals_cache) |intervals| {
self.allocator.free(intervals);
}
if (self.notes_cache) |notes| {
self.allocator.free(notes);
}
}

pub fn getIntervals(self: *Scale) ![]const Interval {
if (self.intervals_cache) |cached_intervals| {
return cached_intervals;
}

const intervals = try self.pattern.getIntervals(self.allocator);
self.intervals_cache = intervals;
return intervals;
}

pub fn getName(self: Scale) []const u8 {
return self.pattern.getName();
}

pub fn getNotes(self: *Scale) ![]const Note {
if (self.notes_cache) |cached_notes| {
return cached_notes;
}

const intervals = try self.getIntervals();
var notes = try self.allocator.alloc(Note, intervals.len);
errdefer self.allocator.free(notes);

// Use a Pitch for internal calculations, but only keep the Note.
const reference_pitch = Pitch{ .note = self.tonic, .octave = 4 };

for (intervals, 0..) |interval, i| {
const pitch = try interval.applyToPitch(reference_pitch);
notes[i] = pitch.note;
}

self.notes_cache = notes;
return notes;
}

pub fn contains(self: *Scale, note: Note) !bool {
const scale_notes = try self.getNotes();
const note_pitch_class = note.getPitchClass();

for (scale_notes) |scale_note| {
if (note_pitch_class == scale_note.getPitchClass()) {
return true;
}
}
return false;
}
};

test "scale creation and note retrieval" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();

var c_major = Scale.init(allocator, Note.c, .major);
defer c_major.deinit();
const notes = try c_major.getNotes();

try testing.expectEqual(Note.c, notes[0]);
try testing.expectEqual(Note.d, notes[1]);
try testing.expectEqual(Note.e, notes[2]);
try testing.expectEqual(Note.f, notes[3]);
try testing.expectEqual(Note.g, notes[4]);
try testing.expectEqual(Note.a, notes[5]);
try testing.expectEqual(Note.b, notes[6]);
try testing.expectEqual(Note.c, notes[7]);
}

test "interval and note caching" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();

var c_major = Scale.init(allocator, Note.c, .major);
defer c_major.deinit();

// First call should compute and cache the results.
const intervals1 = try c_major.getIntervals();
try testing.expect(c_major.intervals_cache != null);
const notes1 = try c_major.getNotes();
try testing.expect(c_major.notes_cache != null);

// Second call should return cached intervals and notes.
const intervals2 = try c_major.getIntervals();
const notes2 = try c_major.getNotes();
try testing.expectEqual(intervals1.ptr, intervals2.ptr);
try testing.expectEqual(notes1.ptr, notes2.ptr);
}

test "scale contains note" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();

var c_major = Scale.init(allocator, Note.c, .major);
defer c_major.deinit();

try testing.expect(try c_major.contains(Note.c));
try testing.expect(try c_major.contains(Note.d));
try testing.expect(try c_major.contains(Note.e));
try testing.expect(try c_major.contains(Note.f));
try testing.expect(try c_major.contains(Note.g));
try testing.expect(try c_major.contains(Note.a));
try testing.expect(try c_major.contains(Note.b));

try testing.expect(!try c_major.contains(Note.c.sharp()));
try testing.expect(!try c_major.contains(Note.f.sharp()));

try testing.expect(try c_major.contains(Note.b.sharp())); // enharmonic to C
try testing.expect(try c_major.contains(Note.e.sharp())); // enharmonic to F
}
64 changes: 64 additions & 0 deletions src/theory/scale_library.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const std = @import("std");
const Interval = @import("interval.zig").Interval;

pub const Pattern = enum {
major,
natural_minor,
harmonic_minor,
melodic_minor,
pentatonic_major,
pentatonic_minor,
chromatic,
dorian,
phrygian,
lydian,
mixolydian,
locrian,
whole_tone,

pub fn getIntervals(self: Pattern, allocator: std.mem.Allocator) ![]const Interval {
const strings = switch (self) {
.major => &[_][]const u8{ "P1", "M2", "M3", "P4", "P5", "M6", "M7", "P8" },
.natural_minor => &[_][]const u8{ "P1", "M2", "m3", "P4", "P5", "m6", "m7", "P8" },
.harmonic_minor => &[_][]const u8{ "P1", "M2", "m3", "P4", "P5", "m6", "M7", "P8" },
.melodic_minor => &[_][]const u8{ "P1", "M2", "m3", "P4", "P5", "M6", "M7", "P8" },
.pentatonic_major => &[_][]const u8{ "P1", "M2", "M3", "P5", "M6", "P8" },
.pentatonic_minor => &[_][]const u8{ "P1", "m3", "P4", "P5", "m7", "P8" },
.chromatic => &[_][]const u8{ "P1", "m2", "M2", "m3", "M3", "P4", "A4", "P5", "m6", "M6", "m7", "M7", "P8" },
.dorian => &[_][]const u8{ "P1", "M2", "m3", "P4", "P5", "M6", "m7", "P8" },
.phrygian => &[_][]const u8{ "P1", "m2", "m3", "P4", "P5", "m6", "m7", "P8" },
.lydian => &[_][]const u8{ "P1", "M2", "M3", "A4", "P5", "M6", "M7", "P8" },
.mixolydian => &[_][]const u8{ "P1", "M2", "M3", "P4", "P5", "M6", "m7", "P8" },
.locrian => &[_][]const u8{ "P1", "m2", "m3", "P4", "d5", "m6", "m7", "P8" },
.whole_tone => &[_][]const u8{ "P1", "M2", "M3", "A4", "A5", "A6", "P8" },
};

var interval_list = try std.ArrayList(Interval).initCapacity(allocator, strings.len);
errdefer interval_list.deinit();

for (strings) |str| {
const interval = try Interval.fromString(str);
try interval_list.append(interval);
}

return interval_list.toOwnedSlice();
}

pub fn getName(self: Pattern) []const u8 {
return switch (self) {
.major => "Major",
.natural_minor => "Natural Minor",
.harmonic_minor => "Harmonic Minor",
.melodic_minor => "Melodic Minor",
.pentatonic_major => "Pentatonic Major",
.pentatonic_minor => "Pentatonic Minor",
.chromatic => "Chromatic",
.dorian => "Dorian",
.phrygian => "Phrygian",
.lydian => "Lydian",
.mixolydian => "Mixolydian",
.locrian => "Locrian",
.whole_tone => "Whole Tone",
};
}
};

0 comments on commit 0a660f4

Please sign in to comment.