Skip to content

Commit

Permalink
Implement a delay estimation algorithm (#34)
Browse files Browse the repository at this point in the history
* rename allowedpicthes to filterpitches

* clean Drums by removing dictionaries

* [wip] translate!

* use filters instead of filter in filterpitches

* bugfix rm_hihat

* implemente + and - for ::Notes

* reorg

* add estimate delay

* test for estimate delay

* changelog
  • Loading branch information
Datseris committed Jul 22, 2019
1 parent a912e60 commit a8420ea
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 73 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ 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).

# 0.9
* new functions `estimate_delay` and `estimate_delay_recursive`.
* Implemented `+` and `-` for `Notes` and `Int`. The operations are identical to `translate`.

# 0.8
* Renamed `allowedpitches` to `filterpiches`.
* Added in-place methods for `translate` and `transpose`.

# v0.7
Rework and big improvement of the function `timeseries`. Firstly, now bins with missing entries get the Julia value `missing` instead of 0. In addition, now one can also get the timeserises of the positions of the data, using the property `:position`. This returns the timing deviations with respect to the corresponding entry in `tvec`. These numbers are also known as *microtiming deviations* in the literature. Finally, the `grid` argument is now mandatory.
See the updated documentation string for more.
Expand Down
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "MusicManipulations"
uuid = "274955c0-c284-5bf7-b122-5ecd51c559de"
repo = "https://github.com/JuliaMusic/MusicManipulations.jl.git"
version = "0.7.0"
version = "0.8.0"

[deps]
DefaultApplication = "3f0dd361-4fe0-5fc6-8523-80b14ec94d85"
Expand All @@ -13,6 +13,6 @@ StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[compat]
MIDI = "1.1.0"
MIDI = "1.4.1"
MotifSequenceGenerator = "≥ 0.2.0"
julia = "1.1"
59 changes: 54 additions & 5 deletions src/data_handling/data_extraction.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export getfirstnotes, allowedpitches, separatepitches, firstnotes
export getfirstnotes, filterpitches, separatepitches, firstnotes

"""
firstnotes(notes, grid)
Expand Down Expand Up @@ -41,18 +41,19 @@ end


"""
allowedpitches(notes::Notes, allowed) -> newnotes
filterpitches(notes::Notes, filters) -> newnotes
Only keep the notes that have a pitch specified in `allowed` (one or many pitches).
Only keep the notes that have a pitch specified in `filters` (one or many pitches).
"""
function allowedpitches(notes::Notes{N}, allowed) where {N<:AbstractNote}
function filterpitches(notes::Notes{N}, filters) where {N<:AbstractNote}
n = N[]
for i 1:length(notes)
notes[i].pitch allowed && push!(n, deepcopy(notes[i]))
notes[i].pitch filters && push!(n, copy(notes[i]))
end
return Notes(n, notes.tpq)
end

@deprecate allowedpitches filterpitches


"""
Expand Down Expand Up @@ -85,3 +86,51 @@ function _add_note_to_dict!(separated, note::N, tpq) where {N<:AbstractNote}
push!(separated[note.pitch], deepcopy(note))
end
end

export estimate_delay, estimate_delay_recursive

estimate_delay(notes, sub::Int) = estimate_delay(notes, 0:(1/sub):1)

"""
estimate_delay(notes, grid)
Estimate the average temporal deviation of the given `notes` from the
quarter note grid point. The notes are classified according to the `grid`
and only notes in the first and last grid bins are used. Their position
is subtracted from the nearby quarter note and the returned value
is the average of this operation.
"""
function estimate_delay(notes::Notes, grid::AbstractVector)
# TODO: this can be optimized by looping over the notes directly
# and classifying one by one, and adding to `d` one by one
clas = classify(notes, grid)
base = notes[findall(x -> x == 1 || x == length(grid), clas)]
d = 0.0
for n in base
pos = Int(n.position % notes.tpq)
res = pos grid[2]*notes.tpq ? pos : pos - notes.tpq
d += res
end
return d / length(base)
end

"""
estimate_delay_recursive(notes, grid, m)
Do the same as [`estimate_delay`](@ref) but for `m` times, while in each step
shifting the notes by the previously found delay. This improves the accuracy
of the algorithm, because the distribution of the quarter notes is estimated
better and better each time. The function should typically converge
after a couple of `m`.
The returned result is the estimated delay, in integer (ticks), as only integers
can be used to actually shift the notes around.
"""
function estimate_delay_recursive(notes, grid, m)
totaldelay = 0
xnotes = notes
for i in 1:m
delay = round(Int, estimate_delay(xnotes, grid))
totaldelay += delay
xnotes = xnotes - delay
end
return totaldelay
end
18 changes: 16 additions & 2 deletions src/general.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Base.transpose

export translate, transpose, randomnotes, subdivision
export velocities, positions, pitches, durations
import Base: transpose, +, -

velocities(notes::Notes) = [Int(x.velocity) for x in notes]
positions(notes::Notes) = [Int(x.position) for x in notes]
Expand Down Expand Up @@ -37,6 +36,21 @@ translate(notes::Notes, ticks) = Notes(translate(notes.notes, ticks), notes.tpq)
translate(notes::Vector{N}, ticks) where {N<:AbstractNote} =
[Note(n.pitch, n.velocity, n.position + ticks, n.duration, n.channel) for n in notes]

"""
translate!(notes, ticks)
In-place version of [`translate`](@ref).
"""
function translate!(notes::Notes, ticks)
for note in notes
note.position += ticks
end
end

+(notes::Notes, x::Real) = translate(notes, round(Int, x))
-(notes::Notes, x::Real) = translate(notes, -round(Int, x))
+(x::Real, notes::Notes) = notes + x
-(x::Real, notes::Notes) = notes - x

"""
transpose(notes, semitones)
Transpose the `notes` for the given amount of `semitones`.
Expand Down
69 changes: 6 additions & 63 deletions src/specific_modules/drums.jl
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,16 @@ end
Remove fake hihat notes generated by certain actions by spotting them and
just removing every Hihat Head event with velocity less than `cutoff_fc`
and position maximum `back` ticks before or `forw` ticks after a foot close.
Triggers to search for are pitches `(0x2c, 0x1a, 0x16)`
(foot close and hi hat rim).
Triggers to search for are pitches are given by the keyword
`triggers = (0x2c, 0x1a, 0x16)` (foot close and hi hat rim).
After that, all notes with pitches in `cut_pitches` and velocity ≤ `cutoff_vel`
are deleted as well, which accounts for spurious note triggering.
"""
function rm_hihatfake!(notes::Notes;
back = 100, forw = 100, cutoff_fc = 0x16,
cutoff_vel = 10, cut_pitches = [0x2e])
cutoff_vel = 10, cut_pitches = [0x2e],
triggers = (0x2c, 0x1a, 0x16))

#first map special closed notes
for note in notes
Expand All @@ -83,7 +84,7 @@ function rm_hihatfake!(notes::Notes;
len = length(notes)
while i <= len
#find foot close
if notes[i].pitch (0x2c, 0x1a, 0x16)
if notes[i].pitch triggers
#go back and remove all fake tip strokes
j = i-1
#search all notes in specified back region
Expand Down Expand Up @@ -125,67 +126,9 @@ function rm_hihatfake!(notes::Notes;
end
deleted += length(deletes)
deleteat!(notes.notes, deletes)
println("rm_hihatfake! deleted $(deleted) fake notes")
println("rm_hihatfake! deleted $(deleted) notes")
end


# Map the pitches of midinotes to Instruments of Roland TD-50
const MAP_TD50 = Dict{UInt8,String}(
0x16=>"Hihat Rim (closed)",
0x1a=>"Hihat Rim",
0x24=>"Kick",
0x25=>"Snare RimClick",
0x26=>"Snare",
0x27=>"Tom 4 Rimshot",
0x28=>"Snare Rimshot",
0x29=>"Tom 4",
0x2a=>"Hihat Head (closed)",
0x2b=>"Tom 3",
0x2c=>"Hihat Foot Close",
0x2d=>"Tom 2",
0x2e=>"Hihat Head",
0x2f=>"Tom 2 Rimshot",
0x30=>"Tom 1",
0x31=>"Cymbal 1",
0x32=>"Tom 1 Rimshot",
0x33=>"Ride Head",
0x34=>"Cymbal 2",
0x35=>"Ride Bell",
0x37=>"Cymbal 1",
0x39=>"Cymbal 2",
0x3a=>"Tom 3 Rimshot",
0x3b=>"Ride Rim")

# All posible pitches in an Array
const ALLPITCHES_TD50 = collect(keys(MAP_TD50))

# Map the pitches to numbers for plotting in a graph
const REORDER_TD50 = Dict{UInt8,UInt8}(
0x16=>8,
0x1a=>5,
0x24=>0,
0x25=>3,
0x26=>1,
0x27=>19,
0x28=>2,
0x29=>18,
0x2a=>7,
0x2b=>16,
0x2c=>6,
0x2d=>14,
0x2e=>4,
0x2f=>15,
0x30=>12,
0x31=>20,
0x32=>13,
0x33=>9,
0x34=>21,
0x35=>11,
0x37=>20,
0x39=>21,
0x3a=>17,
0x3b=>10)

###############################################################################
#velocity quantization
###############################################################################
Expand Down
16 changes: 15 additions & 1 deletion test/data_extraction.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pitchdict = Dict(pitch => count(x->x == pitch, pit) for pitch in pit)
@testset "allowed pitches" begin

allowed = unique(pit)[1:2]
anotes = allowedpitches(rnotes, allowed)
anotes = filterpitches(rnotes, allowed)
@test length(anotes) > 0
@test length(anotes) == pitchdict[allowed[1]] + pitchdict[allowed[2]]
for note anotes
Expand All @@ -24,3 +24,17 @@ end

end
end

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

d = estimate_delay(piano, 0:1//3:1)
@test abs(d) < 30

d2 = estimate_delay_recursive(piano, 0:1//3:1, 5)

@test d2 round(Int, d)
@test abs(d2) < 30
end
end

0 comments on commit a8420ea

Please sign in to comment.