Skip to content

Commit

Permalink
Named note (#65)
Browse files Browse the repository at this point in the history
* add NamedNote

* remove deprecated code

* use eachindex to remove vscode infos

* add NamedNotes

* change version to 1.7

* test change

* remove pitch for NamedNote. Add equality compare

* document change

* add tests

* re-run CI test

* add changelog
  • Loading branch information
NeroBlackstone authored Jun 2, 2023
1 parent 807728e commit fefe123
Show file tree
Hide file tree
Showing 16 changed files with 145 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.

This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

# 1.7.0

* New type `NamedNote`.

# 1.5.0
* Option `missingval` for `timeseries`.

Expand Down
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
name = "MusicManipulations"
uuid = "274955c0-c284-5bf7-b122-5ecd51c559de"
repo = "https://github.com/JuliaMusic/MusicManipulations.jl.git"
version = "1.6.3"
version = "1.7.0"

[deps]
ARFIMA = "9d0fb3db-ba49-4108-bc86-650b3813b6d5"
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
MIDI = "f57c4921-e30c-5f49-b073-3f2f2ada663e"
MotifSequenceGenerator = "7d228ccb-0fc9-53c8-a429-8c9b5b572bc3"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Expand All @@ -18,6 +19,7 @@ MIDI = "1.4.1, 2.0"
MotifSequenceGenerator = "1.0"
Reexport = "0.2, 0.3, 1"
StatsBase = "0.32, 0.33"
DataStructures = "0.18"
julia = "1.1"

[extras]
Expand Down
1 change: 1 addition & 0 deletions src/MusicManipulations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ include("midifiles.jl")
include("quantize.jl")
include("scale.jl")
include("humanize.jl")
include("namednote.jl")

include("data_handling/data_extraction.jl")
include("data_handling/timeseries.jl")
Expand Down
102 changes: 102 additions & 0 deletions src/namednote.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using MIDI: name_to_pitch, pitch_to_name, Note
export NamedNote,NamedNotes

"""
NamedNote(name, velocity, position, duration, channel = 0) <: AbstractNote
Mutable data structure describing a music note with certain pitch name. A bundle of many notes results
in the [`Notes`](@ref) struct, which is the output of the [`getnotes`](@ref)
function.
If the `channel` of the note is `0` (default), it is not shown.
You can also create a `NamedNote` with the following keyword constructor:
```julia
NamedNote(pitch_name::String; position = 0, velocity = 100, duration = 960, channel = 0)
NamedNote(n::Note; pitch_name::String = "")
```
Initialize `Notes{NamedNote}` from a pitch name string, use spaces to separate pitch name.
``` julia
NamedNotes(notes_string::String; tpq::Int = 960)
# NamedNotes("C#6 Db5 E4")
```
Converte `NamedNote` to `Note`:
``` julia
Note(n::NamedNote)
```
Warning: We use attribute `name_to_pitch(name)`,`duration`,`position`,`channel`,`velocity` to compare `NamedNote`. So:
``` julia
NamedNote("Db5") == NamedNote("C#5")
# true
```
## Fields:
* `name::String` : Pitch name, Uppercase.
* `velocity::UInt8` : Dynamic intensity. Cannot be higher than 127 (0x7F).
* `position::UInt` : Position in absolute time (since beginning of track), in ticks.
* `duration::UInt` : Duration in ticks.
* `channel::UInt8 = 0` : Channel of the track that the note is played on.
Cannot be higher than 127 (0x7F).
"""
mutable struct NamedNote <: AbstractNote
name::String
velocity::UInt8
position::UInt
duration::UInt
channel::UInt8

NamedNote(name, velocity, position, duration, channel = 0) =
if channel > 0x7F
error( "Channel must be less than 128" )
elseif velocity > 0x7F
error( "Velocity must be less than 128")
# check if match the regex
elseif !occursin(r"^[A-G][#b♯♭]?\d?$",name)
error("Invalid note pitch name")
else
if !isnumeric(name[end])
name *= "5"
end

# store flat use "♭", store sharp use "♯"
if name[prevind(name,end,1)] == 'b'
name = replace(name,"b"=>"")
elseif name[prevind(name,end,1)] == '#'
name = replace(name,"#"=>"")
end

new(name, velocity, position, duration, channel)
end
end

NamedNote(pitch_name::String; position = 0, velocity = 100, duration = 960, channel = 0) =
NamedNote(pitch_name, velocity, position, duration, channel)

NamedNote(n::Note; pitch_name::String = "") =
length(pitch_name) == 0 ? NamedNote(pitch_to_name(n.pitch), n.position, n.velocity, n.duration, n.channel) : NamedNote(name, n.position, n.velocity, n.duration, n.channel)

NamedNotes(notes_string::String; tpq::Int = 960) = Notes([NamedNote(String(s)) for s in split(notes_string," ")], tpq)

Note(n::NamedNote) = Note(name_to_pitch(n.name), n.position, n.velocity, n.duration, n.channel)

function Base.show(io::IO, note::NamedNote)
nn = rpad(note.name, 3)
chpr = note.channel == 0 ? "" : " | channel $(note.channel)"
velprint = rpad("vel = $(Int(note.velocity))", 9)
print(io, "NamedNote $nn | $velprint | "*
"pos = $(Int(note.position)), "*
"dur = $(Int(note.duration))"*chpr)
end

import Base.==
==(n1::NamedNote, n2::NamedNote) =
name_to_pitch(n1.name) == name_to_pitch(n2.name) &&
n1.duration == n2.duration &&
n1.position == n2.position &&
n1.channel == n2.channel &&
n1.velocity == n2.velocity
2 changes: 1 addition & 1 deletion src/quantize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ end
function classify(notes::Notes, grid)
isgrid(grid)
r = zeros(Int, length(notes))
for i in 1:length(notes)
for i in eachindex(notes)
r[i] = classify(notes[i], grid, notes.tpq)
end
return r
Expand Down
3 changes: 2 additions & 1 deletion src/scale.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using MIDI
using StatsBase
using DataStructures

# export scale_identification, SCALES
export note_to_fundamental, pitch_frequency
Expand Down Expand Up @@ -60,7 +61,7 @@ sorted by most to least frequent.
"""
pitch_frequency(notes) = pitch_frequency(note_to_fundamental(notes))
function pitch_frequency(fundamentals::Vector{String})
occurency = sort(Dict(value => key for (key, value) in countmap(fundamentals)), rev = true)
occurency = sort!(OrderedDict(value => key for (key, value) in countmap(fundamentals)), rev = true)
most_frequent = collect(values(occurency))
return most_frequent
end
Expand Down
2 changes: 1 addition & 1 deletion src/specific_modules/drums.jl
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ function rm_hihatfake!(notes::Notes;
end

deletes = Int[]
for i = 1:length(notes)
for i = eachindex(notes)
if notes[i].pitch in cut_pitches && notes[i].velocity cutoff_vel
push!(deletes, i)
end
Expand Down
2 changes: 1 addition & 1 deletion src/specific_modules/jazz.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function average_swing_ratio(notes::Notes, asr_method::String)
end

# Create Swing ratios array
for i in 1:length(swingnotes)
for i in eachindex(swingnotes)
pos = Int64(swingnotes[i].position)
posmod = mod(pos, tpq)
push!(sr, posmod/(tpq-posmod))
Expand Down
2 changes: 1 addition & 1 deletion test/data_extraction.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ end
end

@testset "estimate delay" begin
for midi in (readMIDIFile("serenade_full.mid"), readMIDIFile(testmidi()))
for midi in (load("serenade_full.mid"), load(testmidi()))
piano = getnotes(midi, 4)

d = estimate_delay(piano, 0:1//3:1)
Expand Down
4 changes: 2 additions & 2 deletions test/midiio.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
let

midi = readMIDIFile("serenade_full.mid")
midi = load("serenade_full.mid")
piano = midi.tracks[4]
notes = getnotes(piano, midi.tpq)

Expand All @@ -22,7 +22,7 @@ end

@testset "Replace Notes" begin

midi = readMIDIFile("serenade_full.mid")
midi = load("serenade_full.mid")
notes2 = getnotes(midi.tracks[2])
newtrack = deepcopy(midi.tracks[2])
newnotes = getnotes(newtrack)
Expand Down
2 changes: 1 addition & 1 deletion test/motif_tests.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@testset "Random Notes Sequence"
@testset "Random Notes Sequence" begin

# Dummy sequence
tpq = 960
Expand Down
20 changes: 20 additions & 0 deletions test/namednote_test.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Test, MusicManipulations
@testset "NamedNote" begin
nn1 = NamedNote("C")
@test nn1.name == "C5"
nn2 = NamedNote("C#6")
@test nn2.name == "C♯6"

@test NamedNote("C♯4").name == "C♯4"

@test NamedNote("Db5") == NamedNote("C#5")

nns = NamedNotes("C#6 Db5 E4")
@test nns[1].name == "C♯6"
@test nns[2].name == "D♭5"
@test nns[3].name == "E4"

@test NamedNote(Note("C#4")) == NamedNote(Note("Db4"))

@test Note(NamedNote("C#4")) == Note(NamedNote("Db4"))
end
4 changes: 2 additions & 2 deletions test/quantizer_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ using Test

let
cd(@__DIR__)
midi = readMIDIFile("serenade_full.mid")
midi = load("serenade_full.mid")
piano = midi.tracks[4]
notes = getnotes(piano, midi.tpq)
tpq = 960
Expand Down Expand Up @@ -86,7 +86,7 @@ end

cd(@__DIR__)
grid = [0,0.383,0.73,1]
midi = readMIDIFile("serenade_full.mid")
midi = load("serenade_full.mid")
notes = getnotes(midi, 4)
tpq = 960
qnotes = quantize(notes, grid)
Expand Down
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ include("timeseries.jl")
include("data_extraction.jl")
include("scale_tests.jl")
include("humanizer_tests.jl")
include("namednote_test.jl")
2 changes: 1 addition & 1 deletion test/scale_tests.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Test
using MusicManipulations: scale_identification

tester = getnotes(readMIDIFile(joinpath(@__DIR__, "recording_uwe_2.mid")),1)
tester = getnotes(load(joinpath(@__DIR__, "recording_uwe_2.mid")),1)
@test scale_identification(tester) == "A♯ Major/G minor"
4 changes: 2 additions & 2 deletions test/timeseries.jl
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ end
end

midipath = dirname(dirname(pathof(MIDI)))*"/test/"
midis = [readMIDIFile(joinpath(@__DIR__, "serenade_full.mid")),
readMIDIFile(joinpath(midipath, "doxy.mid"))
midis = [load(joinpath(@__DIR__, "serenade_full.mid")),
load(joinpath(midipath, "doxy.mid"))
]

@testset "MTD timeseries $i" for i in 1:2
Expand Down

0 comments on commit fefe123

Please sign in to comment.