Skip to content

Commit

Permalink
Notes and intervals
Browse files Browse the repository at this point in the history
  • Loading branch information
dpsanders committed Dec 24, 2023
1 parent 9f13910 commit a8f441f
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 1 deletion.
3 changes: 3 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ uuid = "564e61e1-4667-41b2-a4a2-754a0240c775"
authors = ["David Sanders <dpsanders@gmail.com> and contributors"]
version = "1.0.0-DEV"

[deps]
Luxor = "ae8d54c2-7ccd-5906-9d76-62fc9837b5bc"

[compat]
julia = "1"

Expand Down
17 changes: 16 additions & 1 deletion src/MusicTheory.jl
Original file line number Diff line number Diff line change
@@ -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
89 changes: 89 additions & 0 deletions src/intervals.jl
Original file line number Diff line number Diff line change
@@ -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)
68 changes: 68 additions & 0 deletions src/notes.jl
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit a8f441f

Please sign in to comment.