Skip to content

Commit

Permalink
Refactor notes (#1)
Browse files Browse the repository at this point in the history
* Remove unneeded definitions

* Replace NoteClass with symbol

* Refactor notes

* Add simple tests for notes
  • Loading branch information
dpsanders authored Dec 30, 2023
1 parent 2c44f65 commit 5fe1942
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 70 deletions.
11 changes: 5 additions & 6 deletions src/MusicTheory.jl
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
module MusicTheory

export Pitch, Note, Accidental, NoteClass,
𝄫, ♭, ♮, ♯, 𝄪, 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 Pitch, PitchClass, Accidental, NoteNames
export ♮, ♯, ♭, 𝄪, 𝄫
export accidental, octave

export Interval, IntervalType, Major, Minor, Perfect, Augmented, Diminished,
add_interval, tone, semitone, interval
Expand All @@ -17,6 +16,6 @@ export major_scale, natural_minor_scale, melodic_minor_scale, harmonic_minor_sca


include("notes.jl")
include("intervals.jl")
include("scales.jl")
# include("intervals.jl")
# include("scales.jl")
end
10 changes: 5 additions & 5 deletions src/intervals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function interval(n1::Pitch, n2::Pitch)
n1, n2 = n2, n1
end

note_distance = Int(n2.note.noteclass) - Int(n1.note.noteclass)
note_distance = Int(n2.class.classclass) - Int(n1.class.classclass)

octave_distance = 7 * (n2.octave - n1.octave)

Expand Down Expand Up @@ -88,10 +88,10 @@ interval( (n1, n2) ) = interval(n1, n2)

tone(interval::Interval) = interval.distance

function add_interval(n::Note, interval::Interval)
function add_interval(n::PitchClass, interval::Interval)
new_tone = (tone(n) + tone(interval)) % 7

new_note_class = NoteClass(new_tone)
new_note_class = NoteNames(new_tone)
new_octave = new_tone ÷ 7

new_semitone = semitone(n) + semitone(interval)
Expand All @@ -105,14 +105,14 @@ function add_interval(p::Pitch, interval::Interval)
new_tone = tone(p) + tone(interval)
new_octave = new_tone ÷ 7

new_note = add_interval(p.note, interval)
new_note = add_interval(p.class, interval)
new_pitch = Pitch(new_note, new_octave)

return new_pitch
end

Base.:(+)(p::Pitch, interval::Interval) = add_interval(p, interval)
Base.:(+)(n::Note, interval::Interval) = add_interval(n, interval)
Base.:(+)(n::PitchClass, interval::Interval) = add_interval(n, interval)

const Minor_2nd = Interval(1, Minor)
const Major_2nd = Interval(1, Major)
Expand Down
110 changes: 61 additions & 49 deletions src/notes.jl
Original file line number Diff line number Diff line change
@@ -1,86 +1,66 @@

# often called a "pitch class"
@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)

const note_names = [:C, :D, :E, :F, :G, :A, :B]
const note_semitones = Dict(:C => 0, :D => 2, :E => 4, :F => 5, :G => 7, :A => 9, :B => 11)
const note_to_tone = Dict(v => k for (k, v) in enumerate(note_names))

## 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

struct PitchClass
name::Symbol
accidental::Accidental
end

# default is natural:
Note(noteclass::NoteClass) = Note(noteclass, ♮)
Base.convert(::Type{Note}, noteclass::NoteClass) = Note(noteclass, ♮)
PitchClass(name::Symbol) = PitchClass(name, ♮)
# Base.convert(::Type{PitchClass}, noteclass::NoteNames) = PitchClass(noteclass, ♮)

Base.show(io::IO, note::Note) = print(io, note.noteclass, note.accidental)
subscript(i::Int) = '' + i

"Scientific pitch notation, e.g. C4"
struct Pitch
note::Note
class::PitchClass
octave::Int
end

function Base.show(io::IO, pitch::Pitch)
if pitch.note.accidental ==
print(io, pitch.note.noteclass, pitch.octave)
function Base.show(io::IO, class::PitchClass)
if class.accidental ==
print(io, class.name)
else
print(io, pitch.note, pitch.octave)
print(io, class.name, class.accidental)
end
end

Note(C, ♮)
Base.show(io::IO, pitch::Pitch) =
print(io, pitch.class, subscript(pitch.octave))

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
Base.getindex(class::PitchClass, i::Int) = Pitch(class, i)


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
accidental(class::PitchClass) = class.accidental
accidental(pitch::Pitch) = accidental(pitch.class)

octave(pitch::Pitch) = pitch.octave

tone(note::NoteClass) = Int(note)
tone(note::Note) = tone(note.noteclass)
tone(pitch::Pitch) = tone(pitch.note) + 7 * pitch.octave
tone(note::Symbol) = note_to_tone[note]
tone(note::PitchClass) = tone(note.classclass)
tone(pitch::Pitch) = tone(pitch.class) + 7 * pitch.octave


## Semitones
semitone(accidental::Accidental) = accidental_semitones[accidental]
semitone(note::NoteClass) = note_semitones[note]
semitone(note::Symbol) = note_semitones[note]

semitone(note::Note) = semitone(note.noteclass) + semitone(note.accidental)
semitone(note::PitchClass) = semitone(note.classclass) + semitone(note.accidental)

"Treats C0 as semitone 0"
semitone(pitch::Pitch) = semitone(pitch.note) + 12 * pitch.octave
semitone(pitch::Pitch) = semitone(pitch.class) + 12 * pitch.octave

NoteClass(pitch::Pitch) = pitch.note.noteclass
Note(pitch::Pitch) = pitch.note
PitchClass(pitch::Pitch) = pitch.class


function find_accidental(which_semitone, noteclass)
Expand All @@ -93,13 +73,45 @@ function find_accidental(which_semitone, noteclass)

accidental = semitone_to_accidental[distance]

return Note(noteclass, accidental)
return PitchClass(noteclass, accidental)
end

Note(n::Note) = n
PitchClass(n::PitchClass) = n

const middle_C = C4


Base.isless(n1::Pitch, n2::Pitch) = semitone(n1) < semitone(n2)




# define Julia objects for pitch classes:

module NoteNames

using MusicTheory
using MusicTheory: note_names

export
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𝄪, A𝄪, B𝄪,
middle_C

for name in note_names, accidental in instances(Accidental)
note = Symbol(name, accidental)

@eval $(note) = PitchClass($(Meta.quot(name)), $(accidental))

if accidental ==
@eval $(Symbol(name)) = PitchClass($(Meta.quot(name)), $(accidental))
end
end

const middle_C = C[4]


end
14 changes: 7 additions & 7 deletions src/scales.jl
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
struct Scale{T<:Union{Note, Pitch}}
struct Scale{T<:Union{PitchClass, Pitch}}
tonic::T
steps::Dict{Note, Interval}
steps::Dict{PitchClass, Interval}

function Scale(tonic::T, steps::Vector{Interval}) where {T}
steps_dict = Dict{Note, Interval}()
steps_dict = Dict{PitchClass, Interval}()

current = tonic
for step in steps
steps_dict[Note(current)] = step
steps_dict[PitchClass(current)] = step
current += step
end

Expand All @@ -16,10 +16,10 @@ struct Scale{T<:Union{Note, Pitch}}
end


function Base.iterate(s::Scale{T}, p::T = s.tonic) where {T <: Union{Note, Pitch}}
n = Note(p)
function Base.iterate(s::Scale{T}, p::T = s.tonic) where {T <: Union{PitchClass, Pitch}}
n = PitchClass(p)

!haskey(s.steps, n) && error("Note $n not in scale")
!haskey(s.steps, n) && error("PitchClass $n not in scale")

step = s.steps[n] # an interval
new_p = p + step # a note or a pitch, according to the type of p
Expand Down
4 changes: 1 addition & 3 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using MusicTheory
using Test

@testset "MusicTheory.jl" begin
# Write your tests here.
end
include("notes.jl")

0 comments on commit 5fe1942

Please sign in to comment.