From a8f441f3d5aba5e77f76b422113d29fdd0ea9ef3 Mon Sep 17 00:00:00 2001 From: David Sanders Date: Sun, 24 Dec 2023 14:57:20 -0600 Subject: [PATCH] Notes and intervals --- Project.toml | 3 ++ src/MusicTheory.jl | 17 ++++++++- src/intervals.jl | 89 ++++++++++++++++++++++++++++++++++++++++++++++ src/notes.jl | 68 +++++++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 src/intervals.jl create mode 100644 src/notes.jl diff --git a/Project.toml b/Project.toml index 25015db..bf1bcc1 100644 --- a/Project.toml +++ b/Project.toml @@ -3,6 +3,9 @@ uuid = "564e61e1-4667-41b2-a4a2-754a0240c775" authors = ["David Sanders and contributors"] version = "1.0.0-DEV" +[deps] +Luxor = "ae8d54c2-7ccd-5906-9d76-62fc9837b5bc" + [compat] julia = "1" diff --git a/src/MusicTheory.jl b/src/MusicTheory.jl index 59295c5..958a5d2 100644 --- a/src/MusicTheory.jl +++ b/src/MusicTheory.jl @@ -1,5 +1,20 @@ module MusicTheory -# Write your package code here. +export Pitch, Note, Accidental, NoteClass, Interval, IntervalType, + MajorScale, MinorScale, Major, Minor, Perfect, Augmented, Diminished, + 𝄫, ♭, ♮, ♯, 𝄪, C, D, E, F, G, A, B, C♮, D♮, E♮, F♮, G♮, A♮, B♮, C♯, D♯, E♯, F♯, G♯, A♯, + B♯, C♭, D♭, E♭, F♭, G♭, A♭, B♭, C𝄫, D𝄫, E𝄫, F𝄫, G𝄫, A𝄫, B𝄫, C𝄪, D𝄪, E𝄪, F𝄪, G, + note_names + + +# export all identifiers in this module: +for n in names(@__MODULE__; all=true)♮ + if Base.isidentifier(n) && n ∉ (Symbol(@__MODULE__), :eval, :include) + @eval export $n + end +end + +include("notes.jl") +include("intervals.jl") end diff --git a/src/intervals.jl b/src/intervals.jl new file mode 100644 index 0000000..4d2bd77 --- /dev/null +++ b/src/intervals.jl @@ -0,0 +1,89 @@ +@enum IntervalQuality Perfect Augmented Diminished Major Minor + +# distance is 0 for unison, 1 for second, etc. +# so is 7 for octave, 8 for ninth, etc. +struct Interval + distance::Int + quality::IntervalQuality +end + +function interval_name(distance::Int) + if distance == 0 + return "unison" + elseif distance == 1 + return "2nd" + elseif distance == 2 + return "3rd" + elseif distance == 7 + return "octave" + else + return string(distance + 1, "th") + end +end + +Base.show(io::IO, interval::Interval) = + print(io, interval.quality, " ", interval_name(interval.distance)) + + +interval_semitones = + Dict(0 => 0, 1 => 2, 2 => 4, 3 => 5, 4 => 7, 5 => 9, 6 => 11) + +interval_quality_semitones = Dict( + Perfect => 0, + Augmented => +1, + Diminished => -1, + Major => 0, + Minor => -1 +) + +semitone(interval::Interval) = + interval_semitones[interval.distance] + interval_quality_semitones[interval.quality] + +Base.:(<=)(n1::Pitch, n2::Pitch) = M.semitone(n1) <= M.semitone(n2) + + + +# interval between two pitches: +function interval(n1::Pitch, n2::Pitch) + if n2 <= n1 + n1, n2 = n2, n1 + end + + note_distance = Int(n2.note.noteclass) - Int(n1.note.noteclass) + + octave_distance = 7 * (n2.octave - n1.octave) + + total_note_distance = note_distance + octave_distance + + semitone_distance = (M.semitone(n2) - M.semitone(n1)) % 12 + + base_interval_semitone = interval_semitones[total_note_distance %7] + alteration_distance = semitone_distance - base_interval_semitone + + + + @show note_distance, total_note_distance, semitone_distance, base_interval_semitone, alteration_distance + + if abs(note_distance) + 1 ∈ (1, 4, 5) + if alteration_distance == 0 + return Interval(total_note_distance, Perfect) + elseif alteration_distance >= 1 + return Interval(total_note_distance, Augmented) + elseif alteration_distance <= -1 + return Interval(total_note_distance, Diminished) + end + else + if alteration_distance == 0 + return Interval(total_note_distance, Major) + elseif alteration_distance == -1 + return Interval(total_note_distance, Minor) + elseif alteration_distance > 0 + return Interval(total_note_distance, Augmented) + elseif alteration_distance < -1 + return Interval(total_note_distance, Diminished) + end + end +end + + +interval(M.C4, M.C6) \ No newline at end of file diff --git a/src/notes.jl b/src/notes.jl new file mode 100644 index 0000000..af91c69 --- /dev/null +++ b/src/notes.jl @@ -0,0 +1,68 @@ + +## Names and semitone mappings +@enum NoteClass C=0 D E F G A B + +const note_names = instances(NoteClass) +const scale_grades = Dict(name => value - 1 for (value, name) in enumerate(note_names)) + +# mappings from note names to semitones: +const note_semitones = Dict(C => 0, D => 2, E => 4, F => 5, G => 7, A => 9, B => 11) +const semitone_to_note = Dict(v => k for (k, v) in note_semitones) + + +## Accidentals +@enum Accidental 𝄫 ♭ ♮ ♯ 𝄪 +const accidental_semitones = Dict(𝄫 => -2, ♭ => -1, ♮ => 0, ♯ => 1, 𝄪 => 2) +const semitone_to_accidental = Dict(v => k for (k, v) in accidental_semitones) + + +## Notes +struct Note + noteclass::NoteClass + accidental::Accidental +end + +# default is natural: +Note(noteclass::NoteClass) = Note(noteclass, ♮) +Base.convert(::Type{Note}, noteclass::NoteClass) = Note(noteclass, ♮) + +Base.show(io::IO, note::Note) = print(io, note.noteclass, note.accidental) + +"Scientific pitch notation, e.g. C4" +struct Pitch + note::Note + octave::Int +end + +function Base.show(io::IO, pitch::Pitch) + if pitch.note.accidental == ♮ + print(io, pitch.note.noteclass, pitch.octave) + else + print(io, pitch.note, pitch.octave) + end +end + +Note(C, ♮) + +Base.:*(note::NoteClass, accidental::Accidental) = Note(note, accidental) + +for note in instances(NoteClass), accidental in instances(Accidental) + name = Symbol(note, accidental) + + @eval $(name) = $(note) * $(accidental) +end + + +for note in instances(NoteClass), octave in 0:8 + name = Symbol(note, octave) + @eval $(name) = Pitch($(note), $(octave)) + + for accidental in instances(Accidental) + name = Symbol(note, accidental, octave) + @eval $(name) = Pitch(Note($(note), $(accidental)), $(octave)) + end +end + +octave(pitch::Pitch) = pitch.octave + +const middle_C = C4 \ No newline at end of file