diff --git a/chord.lua b/chord.lua index 345c2c8..1324466 100644 --- a/chord.lua +++ b/chord.lua @@ -307,6 +307,38 @@ function Chord:to_ascending_semitones() return asc_semitones end +function get_nearest_semitones(reference,new) + local nearest_semitones = {} + for k = 1, #new do + local note = new[k] + local nearest = note + local min_distance = math.huge + for _, target_note in ipairs(reference) do + for octave_shift = -1, 1 do + local candidate = note + 12 * octave_shift + local distance = math.abs(candidate - target_note) + if distance < min_distance then + nearest = candidate + min_distance = distance + end + end + end + nearest_semitones[k] = nearest + end + return nearest_semitones +end + +-- Chord inversion that maximizes proximity with a previous chord +-- Returns the notes as semitones that are nearest +-- to the given reference notes +-- The notes are sorted in ascending order +function Chord:to_nearest_semitones_from(reference) + local f = function(n) return n:to_semitone() end + local semitones = self:to_notes():map(f) + return get_nearest_semitones(reference,semitones) +end + + ChordBuilder = {} ChordBuilder.__index = ChordBuilder diff --git a/scale.lua b/scale.lua index 7279e5c..4e6996d 100644 --- a/scale.lua +++ b/scale.lua @@ -127,6 +127,18 @@ function Scale:circle_of_5ths_rotate(iterations) end end +--[[ + Rotates the scale so that the specified note becomes the new root. + This is a permutation of the existing notes, not a transposition. +]] +function Scale:rotate_to_note(target_note) + for i, note in ipairs(self.notes) do + if note.name == target_note.name then + return self:rotate(i) + end + end + return self +end --[[ Returns a new scale based on self, rotated from note_offset @@ -240,4 +252,4 @@ end function Scale:get_note_from_degree(degree) local note = self.notes[degree.name] return Note(note.name, note.alteration + degree.alteration) -end \ No newline at end of file +end diff --git a/test/test_scale.lua b/test/test_scale.lua index 92b8664..3f39fc4 100644 --- a/test/test_scale.lua +++ b/test/test_scale.lua @@ -375,3 +375,20 @@ function TestScale:test_c5ths_rotate() end luaunit.assertEquals(next, c_major) end + +function TestScale:testRotateToNote() + local c_major = Scale.c_major() -- C, D, E, F, G, A, B + + -- Test successful rotation to F (Lydian mode) + local f_note = Note(NotesNames.F, 0) + local rotated = c_major:rotate_to_note(f_note) + luaunit.assertEquals(rotated.notes[1].name, NotesNames.F) + luaunit.assertEquals(rotated.notes[2].name, NotesNames.G) + luaunit.assertEquals(rotated.notes[7].name, NotesNames.E) + + -- Test rotating to current root (C) + local c_note = Note(NotesNames.C, 0) + local rotated_c = c_major:rotate_to_note(c_note) + luaunit.assertEquals(rotated_c.notes[1].name, NotesNames.C) + +end