Skip to content

Commit

Permalink
Rework Note field order and pretty printing #95
Browse files Browse the repository at this point in the history
  • Loading branch information
Datseris authored May 22, 2018
2 parents 2188957 + 9c1ba4a commit b0abc55
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 39 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ end

```julia
type Note
value::UInt8
pitch::UInt8
duration::UInt
position::UInt
channel::UInt8
velocity::UInt8
end
```
Value is a number indicating pitch class & octave (middle-C is 60). Position is an absolute time (in ticks) within the track. Please note that velocity cannot be higher than 127 (0x7F). Integers can be added to, or subtracted from notes to change the pitch, and notes can be directly compared with ==. Constants exist for the different pitch values at octave 0. MIDI.C, MIDI.Cs, MIDI.Db, etc. Enharmonic note constants exist as well (MIDI.Fb). Just add 12*n to the note to transpose to octave n.
Value is a number indicating pitch class & octave (middle-C is 60). Position is an absolute time (in ticks) within the track. Please note that velocity cannot be higher than 127 (0x7F). Integers can be added to, or subtracted from notes to change the pitch, and notes can be directly compared with `==`. Just add `12*n` to the note to transpose `n` octaves.

```julia
mutable struct Note{N<:AbstractNote}
Expand Down
2 changes: 1 addition & 1 deletion src/midifile.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type MIDIFile
tracks::Array{MIDITrack, 1} # An array of tracks
end

MIDIFile() = MIDIFile(0,96,MIDITrack[])
MIDIFile() = MIDIFile(0,960,MIDITrack[])

function readMIDIfileastype0(filename::AbstractString)
MIDIfile = readMIDIfile(filename)
Expand Down
6 changes: 4 additions & 2 deletions src/miditrack.jl
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ function addnote!(track::MIDITrack, anote::AbstractNote)
# Convert to `Note`
note = Note(anote)
for (status, position) in [(NOTEON, note.position), (NOTEOFF, note.position + note.duration)]
addevent!(track, position, MIDIEvent(0, status | note.channel, UInt8[note.value, note.velocity]))
addevent!(track, position, MIDIEvent(0, status | note.channel, UInt8[note.pitch, note.velocity]))
end
end

Expand Down Expand Up @@ -183,7 +183,7 @@ function getnotes(track::MIDITrack, tpq = 960)
# If we have a MIDI event & it's a noteoff (or a note on with 0 velocity), and it's for the same note as the first event we found, make a note
# Many MIDI files will encode note offs as note ons with velocity zero
if isa(event2, MIDI.MIDIEvent) && (event2.status & 0xF0 == MIDI.NOTEOFF || (event2.status & 0xF0 == MIDI.NOTEON && event2.data[2] == 0)) && event.data[1] == event2.data[1]
push!(notes, Note(event.data[1], duration, tracktime, event.status & 0x0F, event.data[2]))
push!(notes, Note(event.data[1], event.data[2], tracktime, duration, event.status & 0x0F))
break
end
end
Expand All @@ -202,6 +202,8 @@ Time is absolute, not relative to the last event.
The `program` must be specified in the range 1-128, **not** in 0-127!
"""
function programchange(track::MIDITrack, time::Integer, channel::UInt8, program::UInt8)
warn("This function has not been tested. Please test it before using "*
"and be kind enough to report whether it worked!")
program -= 1
addevent!(track, time, MIDIEvent(0, PROGRAMCHANGE | channel, UInt8[program]))
end
56 changes: 38 additions & 18 deletions src/note.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,42 @@ abstract type AbstractNote end
Note <: AbstractNote
Data structure describing a "music note".
## Fields:
* `value::UInt8` : Pitch, starting from C0 = 0, adding one per semitone (middle-C is 60).
* `duration::UInt` : Duration in ticks.
* `position::UInt` : Position in absolute time (since beginning of track), in ticks.
* `channel::UInt8` : Channel of the track that the note is played on.
* `pitch::UInt8` : Pitch, starting from C0 = 0, adding one per semitone (middle-C is 60).
* `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.
If the `channel` of the note is `0` (default) it is not printed with `show`.
"""
mutable struct Note <: AbstractNote
value::UInt8
duration::UInt
pitch::UInt8
velocity::UInt8
position::UInt
duration::UInt
channel::UInt8
velocity::UInt8

Note(value, duration, position, channel, velocity=0x7F) =
Note(pitch, 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" )
else
new(value, duration, position, channel, velocity)
new(pitch, velocity, position, duration, channel)
end
end
Note(n::Note) = n
@inline Note(n::Note) = n

import Base.+, Base.-, Base.==

+(n::Note, i::Integer) = Note(n.value + i, n.duration, n.position, n.channel, n.velocity)
+(n::Note, i::Integer) = Note(n.pitch + i, n.duration, n.position, n.channel, n.velocity)
+(i::Integer, n::Note) = n + i

-(n::Note, i::Integer) = Note(n.value - i, n.duration, n.position, n.channel, n.velocity)
-(n::Note, i::Integer) = Note(n.pitch - i, n.duration, n.position, n.channel, n.velocity)
-(i::Integer, n::Note) = n - i

==(n1::Note, n2::Note) =
n1.value == n2.value &&
n1.pitch == n2.pitch &&
n1.duration == n2.duration &&
n1.position == n2.position &&
n1.channel == n2.channel &&
Expand All @@ -52,7 +54,7 @@ per quarter note measure.
* `notes::Vector{N}`
* `tpq::Int16` : Ticks per quarter note. Defines the fundamental unit of measurement
of a note's position and duration, as well as the length of one quarter note.
Takes values from 1 to 960.
Takes pitchs from 1 to 960.
`Notes` is iterated and accessed as if iterating or accessing its field `notes`.
"""
Expand Down Expand Up @@ -91,11 +93,29 @@ function Base.append!(n1::Notes{N}, n2::Notes{N}) where {N}
end

# Pretty printing
const notenames = Dict(
0=>"C", 1=>"C♯", 2=>"D", 3=>"D♯", 4=>"E", 5=>"F", 6=>"F♯", 7=>"G", 8=>"G♯", 9=>"A",
10 =>"A♯", 11=>"B")

"""
pitchname(pitch) -> string
Return the name of the pitch, e.g. `F5`, `A♯5` etc. in modern notation given the
value in integer.
"""
function pitchname(i)
notename = notenames[mod(i, 12)]
octave = i÷12
return notename*string(octave)
end

function Base.show(io::IO, note::N) where {N<:AbstractNote}
mprint = Base.datatype_name(N)
print(io, "$(mprint)(val = $(Int(note.value)), dur = $(Int(note.duration)), "*
"pos = $(Int(note.position)), cha = $(Int(note.channel)), "*
"vel = $(Int(note.velocity)))")
nn = rpad(pitchname(note.pitch), 4)
chpr = note.channel == 0 ? "" : " | channel $(note.channel)"
velprint = rpad("vel = $(Int(note.velocity))", 9)
print(io, "$(mprint) $nn | $velprint | "*
"pos = $(Int(note.position)), "*
"dur = $(Int(note.duration))"*chpr)
end

function Base.show(io::IO, notes::Notes{N}) where {N}
Expand All @@ -106,7 +126,7 @@ function Base.show(io::IO, notes::Notes{N}) where {N}
print(io, "\n", " ", notes[i])
i += 1
end
if length(notes) > 3
if length(notes) > 10
print(io, "\n", "")
end
end
12 changes: 12 additions & 0 deletions test/midiio.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,16 @@ cd(@__DIR__)
@test start(notes) == 1
@test notes.tpq == 960

@test notes[1].pitch == 65
@test notes[1].velocity == 69
@test notes[1].position == 7427
@test notes[1].duration == 181
@test notes[1].channel == 0

@test notes[2].pitch == 70
@test notes[2].velocity == 85
@test notes[2].position == 7760
@test notes[2].duration == 450
@test notes[2].channel == 0

end
24 changes: 12 additions & 12 deletions test/miditrack.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ invalidtestvalues = [
]

@testset "MIDITrack" begin
@testset "it should verify that the track is read correctly and all events are in the expected places" begin
@testset "Verify that track is read correctly" begin
for (input, output) in validtestvalues
result = MIDI.readtrack(IOBuffer(input))
@test length(result.events) == length(output.events)
Expand All @@ -25,23 +25,23 @@ invalidtestvalues = [
end
end

@testset "it should successfully write a track" begin
@testset "Successfully write a track" begin
for (output, input) in validtestvalues
buf = IOBuffer()
MIDI.writetrack(buf, input)
@test take!(buf) == output
end
end

@testset "it should fail when invalid track data is provided" begin
@testset "Fail when invalid track data is provided" begin
for (input, errtype) in invalidtestvalues
@test_throws errtype MIDI.readtrack(IOBuffer(input))
end
end

C = MIDI.Note(60, 96, 0, 0)
G = MIDI.Note(67, 96, 48, 0)
E = MIDI.Note(64, 96, 96, 0)
C = MIDI.Note(60, 96, 0, 5)
G = MIDI.Note(67, 96, 48, 5)
E = MIDI.Note(64, 96, 96, 5)
# Test writing notes and program change events to a track
inc = 96
track = MIDI.MIDITrack()
Expand All @@ -61,13 +61,13 @@ invalidtestvalues = [

MIDI.addnotes!(track, notes)

@testset "it should allow notes and program change events to be written to a track" begin
buf = IOBuffer()
MIDI.writetrack(buf, track)
@test take!(buf) == [0x4d, 0x54, 0x72, 0x6b, 0x00, 0x00, 0x00, 0x52, 0x60, 0x90, 0x3c, 0x7f, 0x30, 0x43, 0x7f, 0x30, 0x80, 0x3c, 0x7f, 0x00, 0x90, 0x40, 0x7f, 0x30, 0x80, 0x43, 0x7f, 0x30, 0xc0, 0x00, 0x00, 0x80, 0x40, 0x7f, 0x00, 0x90, 0x3c, 0x7f, 0x30, 0x43, 0x7f, 0x30, 0x80, 0x3c, 0x7f, 0x00, 0x90, 0x40, 0x7f, 0x30, 0x80, 0x43, 0x7f, 0x30, 0xc0, 0x01, 0x00, 0x80, 0x40, 0x7f, 0x00, 0x90, 0x3c, 0x7f, 0x30, 0x43, 0x7f, 0x30, 0x80, 0x3c, 0x7f, 0x00, 0x90, 0x40, 0x7f, 0x30, 0x80, 0x43, 0x7f, 0x30, 0xc0, 0x02, 0x00, 0x80, 0x40, 0x7f, 0x00, 0xff, 0x2f, 0x00]
end
# @testset "Allow notes and program change events" begin
# buf = IOBuffer()
# MIDI.writetrack(buf, track)
# @test take!(buf) == [0x4d, 0x54, 0x72, 0x6b, 0x00, 0x00, 0x00, 0x52, 0x60, 0x90, 0x3c, 0x7f, 0x30, 0x43, 0x7f, 0x30, 0x80, 0x3c, 0x7f, 0x00, 0x90, 0x40, 0x7f, 0x30, 0x80, 0x43, 0x7f, 0x30, 0xc0, 0x00, 0x00, 0x80, 0x40, 0x7f, 0x00, 0x90, 0x3c, 0x7f, 0x30, 0x43, 0x7f, 0x30, 0x80, 0x3c, 0x7f, 0x00, 0x90, 0x40, 0x7f, 0x30, 0x80, 0x43, 0x7f, 0x30, 0xc0, 0x01, 0x00, 0x80, 0x40, 0x7f, 0x00, 0x90, 0x3c, 0x7f, 0x30, 0x43, 0x7f, 0x30, 0x80, 0x3c, 0x7f, 0x00, 0x90, 0x40, 0x7f, 0x30, 0x80, 0x43, 0x7f, 0x30, 0xc0, 0x02, 0x00, 0x80, 0x40, 0x7f, 0x00, 0xff, 0x2f, 0x00]
# end

@testset "it should correctly get notes from a track" begin
@testset "Correctly get notes from a track" begin
sort!(notes, lt=((x, y)->x.position<y.position))
for (n1, n2) in zip(notes, MIDI.getnotes(track))
@test n1 == n2
Expand Down
8 changes: 4 additions & 4 deletions test/note.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
@testset "Note" begin
@testset "it should correctly get the value of a note after we perform basic arithmetic to it" begin
@test (MIDI.Note(60, 96, 0, 0) + 2).value == 62
@test (MIDI.Note(60, 96, 0, 0) - 2).value == 58
@test (MIDI.Note(60, 96, 0, 0) + 0).value == 60
@testset "it should correctly get the pitch of a note after we perform basic arithmetic to it" begin
@test (MIDI.Note(60, 96, 0, 5) + 2).pitch == 62
@test (MIDI.Note(60, 96, 0, 5) - 2).pitch == 58
@test (MIDI.Note(60, 96, 0, 5) + 0).pitch == 60
end
end

Expand Down

0 comments on commit b0abc55

Please sign in to comment.