music-transcription 0.19.0 → 0.20.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/music-transcription.rb +4 -0
- data/lib/music-transcription/conversion/measure_note_map.rb +42 -0
- data/lib/music-transcription/conversion/measure_score_conversion.rb +152 -0
- data/lib/music-transcription/conversion/tempo_conversion.rb +77 -0
- data/lib/music-transcription/errors.rb +1 -0
- data/lib/music-transcription/model/change.rb +45 -17
- data/lib/music-transcription/model/measure_score.rb +17 -16
- data/lib/music-transcription/model/note.rb +3 -0
- data/lib/music-transcription/model/note_score.rb +8 -6
- data/lib/music-transcription/model/part.rb +1 -1
- data/lib/music-transcription/model/tempo.rb +7 -8
- data/lib/music-transcription/version.rb +1 -1
- data/spec/conversion/measure_note_map_spec.rb +73 -0
- data/spec/conversion/measure_score_conversion_spec.rb +405 -0
- data/spec/conversion/tempo_conversion_spec.rb +152 -0
- data/spec/model/change_spec.rb +75 -1
- data/spec/model/measure_score_spec.rb +2 -0
- data/spec/model/part_spec.rb +0 -3
- metadata +11 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 31245c605d780f4ff342ad6a33f9296a05a1dba1
|
4
|
+
data.tar.gz: 9be01f0138f712d589178163b62d71439f6e0c4f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 80e8d7b289839c11bf75ef05e1786d1fa058c43e82f2ae67e3bae242954dca33fbd2eb2013664e481c41c5f1517a3a38664eabe45fdab187a63f7a6c30c0b917
|
7
|
+
data.tar.gz: dff4b97a87751abafebc8f95364b85b47d453773e838e4c3e896840d2a31b00e32ca6688e444c2a342b96e5b0d7385bba8d602e6055d7e52a2d05d86d5bcdb97
|
data/lib/music-transcription.rb
CHANGED
@@ -46,3 +46,7 @@ require 'music-transcription/packing/part_packing'
|
|
46
46
|
require 'music-transcription/packing/program_packing'
|
47
47
|
require 'music-transcription/packing/note_score_packing'
|
48
48
|
require 'music-transcription/packing/measure_score_packing'
|
49
|
+
|
50
|
+
require 'music-transcription/conversion/tempo_conversion'
|
51
|
+
require 'music-transcription/conversion/measure_note_map'
|
52
|
+
require 'music-transcription/conversion/measure_score_conversion'
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Music
|
2
|
+
module Transcription
|
3
|
+
|
4
|
+
module Conversion
|
5
|
+
# Converte offsets from measure-based to note-based.
|
6
|
+
# @param [Array] measure_offsets Measure offsets to be converted
|
7
|
+
# @param [Hash] measure_durations Map measure durations to measure offsets where the duration takes effect.
|
8
|
+
# @raise [NonZeroError] if first measure duration is not mapped to offset 0
|
9
|
+
def self.measure_note_map measure_offsets, measure_durations
|
10
|
+
mnoff_map = {}
|
11
|
+
moffs = measure_offsets.uniq.sort
|
12
|
+
mdurs = measure_durations.sort
|
13
|
+
cur_noff = 0.to_r
|
14
|
+
j = 0 # next measure offset to be converted
|
15
|
+
|
16
|
+
if mdurs[0][0] != 0
|
17
|
+
raise NonZeroError, "measure offset of 1st measure duration must be 0, not #{mdurs[0][0]}"
|
18
|
+
end
|
19
|
+
|
20
|
+
(0...mdurs.size).each do |i|
|
21
|
+
cur_moff, cur_mdur = mdurs[i]
|
22
|
+
if i < (mdurs.size - 1)
|
23
|
+
next_moff = mdurs[i+1][0]
|
24
|
+
else
|
25
|
+
next_moff = Float::INFINITY
|
26
|
+
end
|
27
|
+
|
28
|
+
while(j < moffs.size && moffs[j] <= next_moff) do
|
29
|
+
moff = moffs[j]
|
30
|
+
mnoff_map[moff] = cur_noff + (moff - cur_moff)*cur_mdur
|
31
|
+
j += 1
|
32
|
+
end
|
33
|
+
|
34
|
+
cur_noff += (next_moff - cur_moff) * cur_mdur
|
35
|
+
end
|
36
|
+
|
37
|
+
return mnoff_map
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module Music
|
2
|
+
module Transcription
|
3
|
+
|
4
|
+
class MeasureScore
|
5
|
+
# Convert to NoteScore object by first converting measure-based offsets to
|
6
|
+
# note-based offsets, and eliminating the use of meters. Also, tempo is
|
7
|
+
# to non-BPM tempo.
|
8
|
+
def to_note_score tempo_class = Tempo::QNPM
|
9
|
+
unless valid?
|
10
|
+
raise NotValidError, "Current MeasureScore is invalid, so it can not be \
|
11
|
+
converted to a NoteScore. Validation errors: #{self.errors}"
|
12
|
+
end
|
13
|
+
|
14
|
+
unless NoteScore.valid_tempo_types.include? tempo_class
|
15
|
+
raise TypeError, "The desired tempo class #{tempo_class} is not valid for a NoteScore."
|
16
|
+
end
|
17
|
+
|
18
|
+
mnoff_map = self.measure_note_map
|
19
|
+
parts = convert_parts(mnoff_map)
|
20
|
+
prog = convert_program(mnoff_map)
|
21
|
+
tcs = convert_tempo_changes(tempo_class, mnoff_map)
|
22
|
+
start_tempo = @start_tempo.convert(tempo_class,@start_meter.beat_duration)
|
23
|
+
|
24
|
+
NoteScore.new(start_tempo, parts: parts, program: prog, tempo_changes: tcs)
|
25
|
+
end
|
26
|
+
|
27
|
+
def measure_note_map
|
28
|
+
Conversion::measure_note_map(measure_offsets,measure_durations)
|
29
|
+
end
|
30
|
+
|
31
|
+
def convert_parts mnoff_map = self.measure_note_map
|
32
|
+
Hash[ @parts.map do |name,part|
|
33
|
+
new_dcs = Hash[ part.dynamic_changes.map do |moff,change|
|
34
|
+
noff = mnoff_map[moff]
|
35
|
+
noff2 = mnoff_map[moff + change.duration]
|
36
|
+
[noff, change.resize(noff2-noff)]
|
37
|
+
end ]
|
38
|
+
new_notes = part.notes.map {|n| n.clone }
|
39
|
+
[name, Part.new(part.start_dynamic,
|
40
|
+
notes: new_notes, dynamic_changes: new_dcs)]
|
41
|
+
end ]
|
42
|
+
end
|
43
|
+
|
44
|
+
def convert_program mnoff_map = self.measure_note_map
|
45
|
+
Program.new(
|
46
|
+
@program.segments.map do |seg|
|
47
|
+
mnoff_map[seg.first]...mnoff_map[seg.last]
|
48
|
+
end
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def convert_tempo_changes tempo_class, mnoff_map = self.measure_note_map
|
53
|
+
tcs = {}
|
54
|
+
bdurs = beat_durations
|
55
|
+
|
56
|
+
@tempo_changes.each do |moff,change|
|
57
|
+
bdur = bdurs.select {|x,y| x <= moff}.max[1]
|
58
|
+
tempo = change.value
|
59
|
+
|
60
|
+
case change
|
61
|
+
when Change::Immediate
|
62
|
+
tcs[mnoff_map[moff]] = Change::Immediate.new(tempo.convert(tempo_class,bdur))
|
63
|
+
when Change::Gradual
|
64
|
+
start_moff, end_moff = moff, moff + change.duration
|
65
|
+
start_noff, end_noff = mnoff_map[start_moff], mnoff_map[end_moff]
|
66
|
+
dur = end_noff - start_noff
|
67
|
+
cur_noff, cur_bdur = start_noff, bdur
|
68
|
+
|
69
|
+
more_bdurs = bdurs.select {|x,y| x > moff && x < end_moff }
|
70
|
+
if more_bdurs.any?
|
71
|
+
more_bdurs.each do |next_moff, next_bdur|
|
72
|
+
next_noff = mnoff_map[next_moff]
|
73
|
+
tcs[cur_noff] = Change::Partial.new(
|
74
|
+
tempo.convert(tempo_class, cur_bdur), dur,
|
75
|
+
cur_noff - start_noff, next_noff - start_noff)
|
76
|
+
cur_noff, cur_bdur = next_noff, next_bdur
|
77
|
+
end
|
78
|
+
tcs[cur_noff] = Change::Partial.new(
|
79
|
+
tempo.convert(tempo_class, cur_bdur), dur,
|
80
|
+
cur_noff - start_noff, dur)
|
81
|
+
else
|
82
|
+
tcs[start_noff] = Change::Gradual.new(
|
83
|
+
tempo.convert(tempo_class, cur_bdur), end_noff - start_noff)
|
84
|
+
end
|
85
|
+
when Change::Partial
|
86
|
+
raise NotImplementedError, "No support yet for converting partial tempo changes."
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
return tcs
|
91
|
+
end
|
92
|
+
|
93
|
+
def beat_duration_at moff
|
94
|
+
beat_durations.select {|k,v| k <= moff }.max[1]
|
95
|
+
end
|
96
|
+
|
97
|
+
def measure_offsets
|
98
|
+
moffs = Set.new([0.to_r])
|
99
|
+
|
100
|
+
@tempo_changes.each do |moff,change|
|
101
|
+
moffs.add(moff)
|
102
|
+
if change.duration > 0
|
103
|
+
moffs.add(moff + change.duration)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
@meter_changes.keys.each {|moff| moffs.add(moff) }
|
108
|
+
|
109
|
+
@parts.values.each do |part|
|
110
|
+
part.dynamic_changes.each do |moff,change|
|
111
|
+
moffs.add(moff)
|
112
|
+
if change.duration > 0
|
113
|
+
moffs.add(moff + change.duration)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
@program.segments.each do |seg|
|
119
|
+
moffs.add(seg.first)
|
120
|
+
moffs.add(seg.last)
|
121
|
+
end
|
122
|
+
|
123
|
+
return moffs.sort
|
124
|
+
end
|
125
|
+
|
126
|
+
def beat_durations
|
127
|
+
bdurs = @meter_changes.map do |offset,change|
|
128
|
+
[ offset, change.value.beat_duration ]
|
129
|
+
end.sort
|
130
|
+
|
131
|
+
if bdurs.empty? || bdurs[0][0] != 0
|
132
|
+
bdurs.unshift([0,@start_meter.beat_duration])
|
133
|
+
end
|
134
|
+
|
135
|
+
return bdurs
|
136
|
+
end
|
137
|
+
|
138
|
+
def measure_durations
|
139
|
+
mdurs = @meter_changes.map do |offset,change|
|
140
|
+
[ offset, change.value.measure_duration ]
|
141
|
+
end.sort
|
142
|
+
|
143
|
+
if mdurs.empty? || mdurs[0][0] != 0
|
144
|
+
mdurs.unshift([0,@start_meter.measure_duration])
|
145
|
+
end
|
146
|
+
|
147
|
+
return Hash[ mdurs ]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Music
|
2
|
+
module Transcription
|
3
|
+
|
4
|
+
class Tempo
|
5
|
+
def convert tgt_class, bdur = nil
|
6
|
+
args = (is_a?(BPM) || tgt_class == BPM) ? [bdur] : []
|
7
|
+
|
8
|
+
return case tgt_class.new(1)
|
9
|
+
when self.class then self.clone
|
10
|
+
when Tempo::QNPM then to_qnpm(*args)
|
11
|
+
when Tempo::NPM then to_npm(*args)
|
12
|
+
when Tempo::NPS then to_nps(*args)
|
13
|
+
when Tempo::BPM then to_bpm(*args)
|
14
|
+
else
|
15
|
+
raise TypeError, "Unexpected target tempo class #{tgt_class}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class QNPM < Tempo
|
20
|
+
def to_npm
|
21
|
+
Tempo::NPM.new(Rational(@value,4))
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_nps
|
25
|
+
Tempo::NPS.new(Rational(@value,240))
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_bpm beat_dur
|
29
|
+
Tempo::BPM.new(Rational(@value,4*beat_dur))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class NPM < Tempo
|
34
|
+
def to_qnpm
|
35
|
+
Tempo::QNPM.new(4*@value)
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_nps
|
39
|
+
Tempo::NPS.new(Rational(@value,60))
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_bpm beat_dur
|
43
|
+
Tempo::BPM.new(Rational(@value,beat_dur))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class BPM < Tempo
|
48
|
+
def to_qnpm beat_dur
|
49
|
+
Tempo::QNPM.new(4*beat_dur*@value)
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_nps beat_dur
|
53
|
+
Tempo::NPS.new(Rational(@value*beat_dur,60))
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_npm beat_dur
|
57
|
+
Tempo::NPM.new(beat_dur*@value)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class NPS < Tempo
|
62
|
+
def to_qnpm
|
63
|
+
Tempo::QNPM.new(Rational(240,@value))
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_bpm beat_dur
|
67
|
+
Tempo::BPM.new(Rational(60,@value*beat_dur))
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_npm
|
71
|
+
Tempo::NPM.new(Rational(60,@value))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -2,7 +2,7 @@ module Music
|
|
2
2
|
module Transcription
|
3
3
|
|
4
4
|
class Change
|
5
|
-
|
5
|
+
attr_reader :value, :duration
|
6
6
|
|
7
7
|
def initialize value, duration
|
8
8
|
@value = value
|
@@ -16,38 +16,66 @@ class Change
|
|
16
16
|
end
|
17
17
|
|
18
18
|
class Immediate < Change
|
19
|
-
include Validatable
|
20
|
-
|
21
19
|
def initialize value
|
22
20
|
super(value,0)
|
23
21
|
end
|
24
22
|
|
25
|
-
def
|
26
|
-
|
23
|
+
def clone
|
24
|
+
Immediate.new(@value)
|
27
25
|
end
|
28
26
|
|
29
|
-
def
|
30
|
-
|
31
|
-
raise NonZeroError, "immediate change duration #{self.duration} must be 0"
|
32
|
-
end
|
27
|
+
def resize newdur
|
28
|
+
self.clone
|
33
29
|
end
|
34
30
|
end
|
35
31
|
|
36
32
|
class Gradual < Change
|
37
|
-
|
33
|
+
def initialize value, transition_dur
|
34
|
+
if transition_dur <= 0
|
35
|
+
raise NonPositiveError, "transition duration #{transition_dur} must be positive"
|
36
|
+
end
|
37
|
+
super(value, transition_dur)
|
38
|
+
end
|
38
39
|
|
39
|
-
def
|
40
|
-
|
40
|
+
def clone
|
41
|
+
Gradual.new(@value,@duration)
|
41
42
|
end
|
42
43
|
|
43
|
-
def
|
44
|
-
|
44
|
+
def resize newdur
|
45
|
+
Gradual.new(@value,newdur)
|
45
46
|
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Partial < Change
|
50
|
+
attr_reader :total_duration, :elapsed, :stop
|
46
51
|
|
47
|
-
def
|
48
|
-
if
|
49
|
-
raise NegativeError, "
|
52
|
+
def initialize value, total_dur, elapsed, stop
|
53
|
+
if elapsed < 0
|
54
|
+
raise NegativeError, "elapsed (#{elapsed}) is < 0"
|
55
|
+
end
|
56
|
+
|
57
|
+
if stop <= 0
|
58
|
+
raise NonPositiveError, "stop (#{stop}) is < 0"
|
50
59
|
end
|
60
|
+
|
61
|
+
if stop > total_dur
|
62
|
+
raise ArgumentError, "stop (#{stop}) is > total duration (#{total_dur})"
|
63
|
+
end
|
64
|
+
|
65
|
+
if stop <= elapsed
|
66
|
+
raise ArgumentError, "stop (#{stop}) is <= elapsed (#{elapsed})"
|
67
|
+
end
|
68
|
+
|
69
|
+
@total_duration = total_dur
|
70
|
+
@elapsed = elapsed
|
71
|
+
@stop = stop
|
72
|
+
super(value,stop - elapsed)
|
73
|
+
end
|
74
|
+
|
75
|
+
def ==(other)
|
76
|
+
super() &&
|
77
|
+
@elapsed == other.elapsed &&
|
78
|
+
@stop == other.stop
|
51
79
|
end
|
52
80
|
end
|
53
81
|
end
|
@@ -8,21 +8,22 @@ class MeasureScore < NoteScore
|
|
8
8
|
@start_meter = start_meter
|
9
9
|
@meter_changes = meter_changes
|
10
10
|
|
11
|
-
super(start_tempo, tempo_changes: tempo_changes,
|
11
|
+
super(start_tempo, tempo_changes: tempo_changes,
|
12
|
+
program: program, parts: parts)
|
12
13
|
yield(self) if block_given?
|
13
14
|
end
|
14
15
|
|
15
16
|
def check_methods
|
16
|
-
super() + [:check_startmeter_type, :check_meterchange_types,
|
17
|
+
super() + [:check_startmeter_type, :check_meterchange_types,
|
18
|
+
:check_meterchange_durs, :check_meterchange_offsets]
|
17
19
|
end
|
18
20
|
|
19
21
|
def validatables
|
20
|
-
super() + [ @start_meter ] + @meter_changes.values
|
21
|
-
@meter_changes.values.map {|v| v.value}
|
22
|
+
super() + [ @start_meter ] + @meter_changes.values.map {|v| v.value}
|
22
23
|
end
|
23
24
|
|
24
|
-
def valid_tempo_types
|
25
|
-
|
25
|
+
def self.valid_tempo_types
|
26
|
+
NoteScore.valid_tempo_types + [ Tempo::BPM ]
|
26
27
|
end
|
27
28
|
|
28
29
|
def check_startmeter_type
|
@@ -38,24 +39,24 @@ class MeasureScore < NoteScore
|
|
38
39
|
end
|
39
40
|
end
|
40
41
|
|
42
|
+
def check_meterchange_offsets
|
43
|
+
badoffsets = @meter_changes.select {|k,v| k != k.to_i }
|
44
|
+
if badoffsets.any?
|
45
|
+
raise NonIntegerError, "meter changes #{badoffsets} have non-integer offsets"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
41
49
|
def check_meterchange_durs
|
42
|
-
nonzero_duration = @meter_changes.select {|k,v| v.
|
50
|
+
nonzero_duration = @meter_changes.select {|k,v| !v.is_a?(Change::Immediate) }
|
43
51
|
if nonzero_duration.any?
|
44
|
-
raise NonZeroError, "meter changes #{nonzero_duration}
|
52
|
+
raise NonZeroError, "meter changes #{nonzero_duration} are not immediate"
|
45
53
|
end
|
46
54
|
end
|
47
55
|
|
48
56
|
def ==(other)
|
49
57
|
return super() && @start_meter == other.start_meter &&
|
50
58
|
@meter_changes == other.meter_changes
|
51
|
-
end
|
52
|
-
|
53
|
-
# Convert to NoteScore object by first converting measure-based offsets to
|
54
|
-
# note-based offsets, and eliminating the use of meters. Also, tempo is
|
55
|
-
# converted from beats-per-minute to notes-per-minute.
|
56
|
-
def to_note_score
|
57
|
-
|
58
|
-
end
|
59
|
+
end
|
59
60
|
end
|
60
61
|
|
61
62
|
end
|
@@ -13,6 +13,9 @@ class Note
|
|
13
13
|
|
14
14
|
def initialize duration, pitches = [], articulation: DEFAULT_ARTICULATION, accented: false, links: {}
|
15
15
|
@duration = duration
|
16
|
+
if !pitches.is_a? Enumerable
|
17
|
+
pitches = [ pitches ]
|
18
|
+
end
|
16
19
|
@pitches = Set.new(pitches).sort
|
17
20
|
@articulation = articulation
|
18
21
|
@accented = accented
|
@@ -20,23 +20,25 @@ class NoteScore
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def validatables
|
23
|
-
[ @program ] + @
|
23
|
+
[ @program ] + @parts.values
|
24
24
|
end
|
25
25
|
|
26
|
-
def valid_tempo_types
|
26
|
+
def self.valid_tempo_types
|
27
27
|
[ Tempo::QNPM, Tempo::NPM, Tempo::NPS ]
|
28
28
|
end
|
29
29
|
|
30
30
|
def check_start_tempo_type
|
31
|
-
|
32
|
-
|
31
|
+
vtts = self.class.valid_tempo_types
|
32
|
+
unless vtts.include?(@start_tempo.class)
|
33
|
+
raise TypeError, "type of start tempo #{@start_tempo} is not one of valid tempo types: #{vtts}"
|
33
34
|
end
|
34
35
|
end
|
35
36
|
|
36
37
|
def check_tempo_change_types
|
37
|
-
|
38
|
+
vtts = self.class.valid_tempo_types
|
39
|
+
baddtypes = @tempo_changes.select {|k,v| !vtts.include?(v.value.class) }
|
38
40
|
if baddtypes.any?
|
39
|
-
raise NonPositiveError, "type of tempo change values #{baddtypes} are not one of valid tempo types: #{
|
41
|
+
raise NonPositiveError, "type of tempo change values #{baddtypes} are not one of valid tempo types: #{vtts}"
|
40
42
|
end
|
41
43
|
end
|
42
44
|
|
@@ -12,15 +12,14 @@ class Tempo
|
|
12
12
|
self.class == other.class && self.value == other.value
|
13
13
|
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
def to_s
|
18
|
-
"#{@value}#{self.class::PRINT_SYM}"
|
19
|
-
end
|
20
|
-
end
|
21
|
-
klass.const_set(:PRINT_SYM,sym)
|
22
|
-
Tempo.const_set(sym.upcase,klass)
|
15
|
+
def clone
|
16
|
+
self.class.new(@value)
|
23
17
|
end
|
18
|
+
|
19
|
+
class QNPM < Tempo; def to_s; "#{@value}qnpm" end; end
|
20
|
+
class NPM < Tempo; def to_s; "#{@value}npm" end; end
|
21
|
+
class BPM < Tempo; def to_s; "#{@value}bpm" end; end
|
22
|
+
class NPS < Tempo; def to_s; "#{@value}nps" end; end
|
24
23
|
end
|
25
24
|
|
26
25
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe 'Conversion.measure_note_map' do
|
4
|
+
before :all do
|
5
|
+
mdurs = Hash[ [[0, (3/4)], [1, (1/2)], [3, (3/4)]] ]
|
6
|
+
@moffs = [ 0, 1, 3, 4, 5, 6, 7, 8, 11, 14, 17, 20, (45/2)]
|
7
|
+
@mnoff_map = Conversion.measure_note_map(@moffs,mdurs)
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should return a Hash' do
|
11
|
+
@mnoff_map.should be_a Hash
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should have same size as array returned by #measure_offsets' do
|
15
|
+
@mnoff_map.size.should eq(@moffs.size)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should have a key for each offset in the array returned by #measure_offsets' do
|
19
|
+
@mnoff_map.keys.sort.should eq(@moffs)
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'single measure duration at 0' do
|
23
|
+
it 'should mutiply all measure offsets by start measure duration' do
|
24
|
+
[TWO_FOUR,SIX_EIGHT,FOUR_FOUR,THREE_FOUR].each do |meter|
|
25
|
+
mdur = meter.measure_duration
|
26
|
+
mdurs = { 0 => mdur }
|
27
|
+
tgt = @moffs.map {|moff| moff * mdur}
|
28
|
+
Conversion.measure_note_map(@moffs,mdurs).values.sort.should eq(tgt)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context '1 meter change' do
|
34
|
+
before :all do
|
35
|
+
@first_mc_off = 3
|
36
|
+
@start_meter = THREE_FOUR
|
37
|
+
@new_meter = TWO_FOUR
|
38
|
+
@score = MeasureScore.new(@start_meter, Tempo::BPM.new(120),
|
39
|
+
meter_changes: { @first_mc_off => Change::Immediate.new(@new_meter) },
|
40
|
+
tempo_changes: {
|
41
|
+
"1/2".to_r => Change::Gradual.new(Tempo::BPM.new(100),1),
|
42
|
+
2 => Change::Immediate.new(Tempo::BPM.new(120)),
|
43
|
+
3 => Change::Immediate.new(Tempo::BPM.new(100)),
|
44
|
+
3.1 => Change::Gradual.new(Tempo::BPM.new(100),1),
|
45
|
+
5 => Change::Immediate.new(Tempo::BPM.new(120)),
|
46
|
+
6 => Change::Immediate.new(Tempo::BPM.new(100)),
|
47
|
+
}
|
48
|
+
)
|
49
|
+
@moffs = @score.measure_offsets
|
50
|
+
@mdurs = @score.measure_durations
|
51
|
+
@mnoff_map = Conversion.measure_note_map(@moffs,@mdurs)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should mutiply all measure offsets that occur on or before 1st meter change offset by start measure duration' do
|
55
|
+
moffs = @moffs.select{ |x| x <= @first_mc_off }
|
56
|
+
tgt = moffs.map do |moff|
|
57
|
+
moff * @start_meter.measure_duration
|
58
|
+
end.sort
|
59
|
+
src = @mnoff_map.select {|k,v| k <= @first_mc_off }
|
60
|
+
src.values.sort.should eq(tgt)
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should, for any measure offsets occurring after 1st meter change offset, add 1st_meter_change_offset * 1st_measure_duration to \
|
64
|
+
new_measure_duration * (offset - 1st_meter_change_offset)' do
|
65
|
+
moffs = @moffs.select{ |x| x > @first_mc_off }
|
66
|
+
tgt = moffs.map do |moff|
|
67
|
+
@first_mc_off * @start_meter.measure_duration + (moff - @first_mc_off) * @new_meter.measure_duration
|
68
|
+
end.sort
|
69
|
+
src = @mnoff_map.select {|k,v| k > @first_mc_off }
|
70
|
+
src.values.sort.should eq(tgt)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,405 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe MeasureScore do
|
4
|
+
before :all do
|
5
|
+
@parts = {
|
6
|
+
"piano" => Part.new(Dynamics::MP,
|
7
|
+
notes: [Note.quarter(C4), Note.eighth(F3), Note.whole(C4), Note.half(D4)]*12,
|
8
|
+
dynamic_changes: {
|
9
|
+
1 => Change::Immediate.new(Dynamics::MF),
|
10
|
+
5 => Change::Immediate.new(Dynamics::FF),
|
11
|
+
6 => Change::Gradual.new(Dynamics::MF,2),
|
12
|
+
14 => Change::Immediate.new(Dynamics::PP),
|
13
|
+
}
|
14
|
+
)
|
15
|
+
}
|
16
|
+
@prog = Program.new([0...3,4...7,1...20,17..."45/2".to_r])
|
17
|
+
tcs = {
|
18
|
+
0 => Change::Immediate.new(Tempo::BPM.new(120)),
|
19
|
+
4 => Change::Gradual.new(Tempo::BPM.new(60),2),
|
20
|
+
11 => Change::Immediate.new(Tempo::BPM.new(110))
|
21
|
+
}
|
22
|
+
mcs = {
|
23
|
+
1 => Change::Immediate.new(TWO_FOUR),
|
24
|
+
3 => Change::Immediate.new(SIX_EIGHT)
|
25
|
+
}
|
26
|
+
@score = MeasureScore.new(THREE_FOUR, Tempo::BPM.new(120),
|
27
|
+
parts: @parts,
|
28
|
+
program: @prog,
|
29
|
+
tempo_changes: tcs,
|
30
|
+
meter_changes: mcs
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#measure_offsets' do
|
35
|
+
before(:all){ @moffs = @score.measure_offsets }
|
36
|
+
|
37
|
+
it 'should return an already-sorted array' do
|
38
|
+
@moffs.should eq @moffs.sort
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should start with offset from start tempo/meter/dynamic' do
|
42
|
+
@moffs[0].should eq(0)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should include offsets from tempo changes' do
|
46
|
+
@score.tempo_changes.each do |moff,change|
|
47
|
+
@moffs.should include(moff)
|
48
|
+
@moffs.should include(moff + change.duration)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should include offsets from meter changes' do
|
53
|
+
@score.meter_changes.keys.each {|moff| @moffs.should include(moff) }
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should include offsets from each part's dynamic changes" do
|
57
|
+
@score.parts.values.each do |part|
|
58
|
+
part.dynamic_changes.each do |moff,change|
|
59
|
+
@moffs.should include(moff)
|
60
|
+
@moffs.should include(moff + change.duration)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should include offsets from program segments' do
|
66
|
+
@score.program.segments.each do |seg|
|
67
|
+
@moffs.should include(seg.first)
|
68
|
+
@moffs.should include(seg.last)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe '#measure_durations' do
|
74
|
+
before(:all){ @mdurs = @score.measure_durations }
|
75
|
+
|
76
|
+
it 'should return a Hash' do
|
77
|
+
@mdurs.should be_a Hash
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'no meter change at offset 0' do
|
81
|
+
it 'should have size of meter_changes.size + 1' do
|
82
|
+
@mdurs.size.should eq(@score.meter_changes.size + 1)
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should begin with offset 0' do
|
86
|
+
@mdurs.keys.min.should eq(0)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'should map start meter to offset 0' do
|
90
|
+
@mdurs[0].should eq(@score.start_meter.measure_duration)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'meter change at offset 0' do
|
95
|
+
before :all do
|
96
|
+
@change = Change::Immediate.new(THREE_FOUR)
|
97
|
+
@score2 = MeasureScore.new(FOUR_FOUR, Tempo::BPM.new(120), meter_changes: { 0 => @change })
|
98
|
+
@mdurs2 = @score2.measure_durations
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should have same size as meter changes' do
|
102
|
+
@mdurs2.size.should eq(@score2.meter_changes.size)
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'should begin with offset 0' do
|
106
|
+
@mdurs2.keys.min.should eq(0)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'should begin with meter change at offset 0, instead of start meter' do
|
110
|
+
@mdurs2[0].should eq(@change.value.measure_duration)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'no meter changes' do
|
115
|
+
before :all do
|
116
|
+
@score3 = MeasureScore.new(FOUR_FOUR, Tempo::BPM.new(120))
|
117
|
+
@mdurs3 = @score3.measure_durations
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should have size 1' do
|
121
|
+
@mdurs3.size.should eq(1)
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'should begin with offset 0' do
|
125
|
+
@mdurs3.keys.min.should eq(0)
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'should begin with start meter' do
|
129
|
+
@mdurs3[0].should eq(@score3.start_meter.measure_duration)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe '#covert_parts' do
|
135
|
+
before :each do
|
136
|
+
@changeA = Change::Immediate.new(Dynamics::PP)
|
137
|
+
@changeB = Change::Gradual.new(Dynamics::F, 2)
|
138
|
+
@score = MeasureScore.new(FOUR_FOUR, Tempo::BPM.new(120),
|
139
|
+
parts: {"simple" => Part.new(Dynamics::MP, dynamic_changes: { 1 => @changeA, 3 => @changeB })}
|
140
|
+
)
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'should return Hash with original part names' do
|
144
|
+
parts = @score.convert_parts
|
145
|
+
parts.should be_a Hash
|
146
|
+
parts.keys.sort.should eq(@score.parts.keys.sort)
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'should convert part dynamic change offsets from measure-based to note-based' do
|
150
|
+
parts = @score.convert_parts
|
151
|
+
parts.should have_key("simple")
|
152
|
+
part = parts["simple"]
|
153
|
+
part.dynamic_changes.keys.sort.should eq([1,3])
|
154
|
+
change = part.dynamic_changes[Rational(1,1)]
|
155
|
+
change.value.should eq(@changeA.value)
|
156
|
+
change.duration.should eq(0)
|
157
|
+
change = part.dynamic_changes[Rational(3,1)]
|
158
|
+
change.value.should eq(@changeB.value)
|
159
|
+
change.duration.should eq(2)
|
160
|
+
|
161
|
+
@score.start_meter = THREE_FOUR
|
162
|
+
parts = @score.convert_parts
|
163
|
+
parts.should have_key("simple")
|
164
|
+
part = parts["simple"]
|
165
|
+
part.dynamic_changes.keys.sort.should eq([Rational(3,4),Rational(9,4)])
|
166
|
+
change = part.dynamic_changes[Rational(3,4)]
|
167
|
+
change.value.should eq(@changeA.value)
|
168
|
+
change.duration.should eq(0)
|
169
|
+
change = part.dynamic_changes[Rational(9,4)]
|
170
|
+
change.value.should eq(@changeB.value)
|
171
|
+
change.duration.should eq(1.5)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe '#convert_program' do
|
176
|
+
before :each do
|
177
|
+
@prog = Program.new([0...4,2...5])
|
178
|
+
@score = MeasureScore.new(FOUR_FOUR, Tempo::BPM.new(120), program: @prog)
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'shuld return Program with same number of segments' do
|
182
|
+
prog = @score.convert_program
|
183
|
+
prog.should be_a Program
|
184
|
+
prog.segments.size.should eq(@score.program.segments.size)
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'should convert program segments offsets from measure-based to note-based' do
|
188
|
+
prog = @score.convert_program
|
189
|
+
prog.segments.size.should eq(2)
|
190
|
+
prog.segments[0].first.should eq(0)
|
191
|
+
prog.segments[0].last.should eq(4)
|
192
|
+
prog.segments[1].first.should eq(2)
|
193
|
+
prog.segments[1].last.should eq(5)
|
194
|
+
|
195
|
+
@score.start_meter = THREE_FOUR
|
196
|
+
prog = @score.convert_program
|
197
|
+
prog.segments.size.should eq(2)
|
198
|
+
prog.segments[0].first.should eq(0)
|
199
|
+
prog.segments[0].last.should eq(3)
|
200
|
+
prog.segments[1].first.should eq(1.5)
|
201
|
+
prog.segments[1].last.should eq(3.75)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
describe '#convert_tempo_changes' do
|
206
|
+
context 'immediate tempo changes' do
|
207
|
+
before :all do
|
208
|
+
@score = MeasureScore.new(THREE_FOUR, Tempo::BPM.new(120),
|
209
|
+
tempo_changes: { 1 => Change::Immediate.new(Tempo::BPM.new(100)),
|
210
|
+
2.5 => Change::Immediate.new(Tempo::NPS.new(1.5)),
|
211
|
+
4 => Change::Immediate.new(Tempo::NPM.new(22.5)),
|
212
|
+
7 => Change::Immediate.new(Tempo::QNPM.new(90)) }
|
213
|
+
)
|
214
|
+
@tempo_type = Tempo::QNPM
|
215
|
+
@tcs = @score.convert_tempo_changes(@tempo_type)
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'should change offset from measure-based to note-based' do
|
219
|
+
@tcs.keys.sort.should eq([0.75, 1.875, 3, 5.25])
|
220
|
+
end
|
221
|
+
|
222
|
+
it 'should convert tempo type to given type' do
|
223
|
+
@tcs.values.each {|change| change.value.should be_a @tempo_type }
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
context 'gradual tempo changes' do
|
228
|
+
context 'no meter changes within tempo change duration' do
|
229
|
+
before :all do
|
230
|
+
@score = MeasureScore.new(THREE_FOUR, Tempo::BPM.new(120),
|
231
|
+
tempo_changes: { 2 => Change::Gradual.new(Tempo::BPM.new(100),2) },
|
232
|
+
meter_changes: { 1 => Change::Immediate.new(TWO_FOUR),
|
233
|
+
4 => Change::Immediate.new(SIX_EIGHT) }
|
234
|
+
)
|
235
|
+
@tempo_type = Tempo::QNPM
|
236
|
+
@tcs = @score.convert_tempo_changes(@tempo_type)
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'should change tempo change offset to note-based' do
|
240
|
+
@tcs.keys.should eq([Rational(5,4)])
|
241
|
+
end
|
242
|
+
|
243
|
+
it 'should convert the tempo change' do
|
244
|
+
@tcs[Rational(5,4)].value.should be_a @tempo_type
|
245
|
+
end
|
246
|
+
|
247
|
+
it 'should convert change duration to note-based' do
|
248
|
+
@tcs[Rational(5,4)].duration.should eq(1)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
context 'single meter change within tempo change duration' do
|
253
|
+
before :all do
|
254
|
+
@tc_moff, @mc_moff = 2, 4
|
255
|
+
@tc_dur = 4
|
256
|
+
@score = MeasureScore.new(THREE_FOUR, Tempo::BPM.new(120),
|
257
|
+
tempo_changes: { @tc_moff => Change::Gradual.new(Tempo::BPM.new(100),@tc_dur) },
|
258
|
+
meter_changes: { @mc_moff => Change::Immediate.new(SIX_EIGHT) }
|
259
|
+
)
|
260
|
+
@tempo_type = Tempo::QNPM
|
261
|
+
@tcs = @score.convert_tempo_changes(@tempo_type)
|
262
|
+
@mnoff_map = @score.measure_note_map
|
263
|
+
end
|
264
|
+
|
265
|
+
it 'should split the one gradual change into two partial changes' do
|
266
|
+
@tcs.size.should eq(2)
|
267
|
+
@tcs.values.each {|x| x.should be_a Change::Partial }
|
268
|
+
end
|
269
|
+
|
270
|
+
it 'should start first partial change where gradual change would start' do
|
271
|
+
@tcs.should have_key(@mnoff_map[@tc_moff])
|
272
|
+
end
|
273
|
+
|
274
|
+
it 'should stop first partial, and start second partial change where inner meter change occurs' do
|
275
|
+
pc1_start_noff = @mnoff_map[@tc_moff]
|
276
|
+
pc1_end_noff = pc1_start_noff + @tcs[pc1_start_noff].duration
|
277
|
+
|
278
|
+
pc2_start_noff = @mnoff_map[@mc_moff]
|
279
|
+
@tcs.should have_key(pc2_start_noff)
|
280
|
+
pc1_end_noff.should eq(pc2_start_noff)
|
281
|
+
end
|
282
|
+
|
283
|
+
it 'should stop second partial change where gradual change would end' do
|
284
|
+
pc2_start_noff = @mnoff_map[@mc_moff]
|
285
|
+
pc2_end_noff = pc2_start_noff + @tcs[pc2_start_noff].duration
|
286
|
+
pc2_end_noff.should eq(@mnoff_map[@tc_moff + @tc_dur])
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
context 'two meter changes within tempo change duration' do
|
291
|
+
before :all do
|
292
|
+
@tc_moff, @mc1_moff, @mc2_moff = 2, 4, 5
|
293
|
+
@tc_dur = 5
|
294
|
+
@score = MeasureScore.new(THREE_FOUR, Tempo::BPM.new(120),
|
295
|
+
tempo_changes: { @tc_moff => Change::Gradual.new(Tempo::BPM.new(100),@tc_dur) },
|
296
|
+
meter_changes: { @mc1_moff => Change::Immediate.new(SIX_EIGHT),
|
297
|
+
@mc2_moff => Change::Immediate.new(TWO_FOUR) }
|
298
|
+
)
|
299
|
+
@tempo_type = Tempo::QNPM
|
300
|
+
@tcs = @score.convert_tempo_changes(@tempo_type)
|
301
|
+
@mnoff_map = @score.measure_note_map
|
302
|
+
end
|
303
|
+
|
304
|
+
it 'should split the one gradual change into three partial changes' do
|
305
|
+
@tcs.size.should eq(3)
|
306
|
+
@tcs.values.each {|x| x.should be_a Change::Partial }
|
307
|
+
end
|
308
|
+
|
309
|
+
it 'should start first partial change where gradual change would start' do
|
310
|
+
@tcs.should have_key(@mnoff_map[@tc_moff])
|
311
|
+
end
|
312
|
+
|
313
|
+
it 'should stop first partial, and start second partial change where 1st meter change occurs' do
|
314
|
+
pc1_start_noff = @mnoff_map[@tc_moff]
|
315
|
+
pc1_end_noff = pc1_start_noff + @tcs[pc1_start_noff].duration
|
316
|
+
|
317
|
+
pc2_start_noff = @mnoff_map[@mc1_moff]
|
318
|
+
@tcs.should have_key(pc2_start_noff)
|
319
|
+
pc1_end_noff.should eq(pc2_start_noff)
|
320
|
+
end
|
321
|
+
|
322
|
+
it 'should stop second partial, and start third partial change where 2st meter change occurs' do
|
323
|
+
pc2_start_noff = @mnoff_map[@mc1_moff]
|
324
|
+
pc2_end_noff = pc2_start_noff + @tcs[pc2_start_noff].duration
|
325
|
+
|
326
|
+
pc3_start_noff = @mnoff_map[@mc2_moff]
|
327
|
+
@tcs.should have_key(pc3_start_noff)
|
328
|
+
pc2_end_noff.should eq(pc3_start_noff)
|
329
|
+
end
|
330
|
+
|
331
|
+
it 'should stop third partial change where gradual change would end' do
|
332
|
+
pc3_start_noff = @mnoff_map[@mc2_moff]
|
333
|
+
pc3_end_noff = pc3_start_noff + @tcs[pc3_start_noff].duration
|
334
|
+
pc3_end_noff.should eq(@mnoff_map[@tc_moff + @tc_dur])
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
context 'partial tempo changes' do
|
340
|
+
it 'should raise NotImplementedError' do
|
341
|
+
@score = MeasureScore.new(THREE_FOUR, Tempo::BPM.new(120),
|
342
|
+
tempo_changes: { 1 => Change::Partial.new(Tempo::BPM.new(100),10,2,3)}
|
343
|
+
)
|
344
|
+
expect { @score.convert_tempo_changes(Tempo::QNPM) }.to raise_error(NotImplementedError)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
describe '#to_note_score' do
|
350
|
+
context 'current score is invalid' do
|
351
|
+
it 'should raise NotValidError' do
|
352
|
+
score = MeasureScore.new(1, Tempo::BPM.new(120))
|
353
|
+
expect { score.to_note_score }.to raise_error(NotValidError)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
context 'given desired tempo class is not valid for NoteScore' do
|
358
|
+
it 'should raise TypeError' do
|
359
|
+
score = MeasureScore.new(FOUR_FOUR, Tempo::BPM.new(120))
|
360
|
+
expect {score.to_note_score(Tempo::BPM) }.to raise_error(TypeError)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
it 'should return a NoteScore' do
|
365
|
+
score = MeasureScore.new(FOUR_FOUR, Tempo::BPM.new(120))
|
366
|
+
score.to_note_score(Tempo::QNPM).should be_a NoteScore
|
367
|
+
end
|
368
|
+
|
369
|
+
it 'should convert start tempo according to given desired tempo class' do
|
370
|
+
score = MeasureScore.new(FOUR_FOUR, Tempo::BPM.new(120))
|
371
|
+
{ Tempo::QNPM => 120, Tempo::NPM => 30, Tempo::NPS => 0.5 }.each do |tempo_class, tgt_val|
|
372
|
+
nscore = score.to_note_score(tempo_class)
|
373
|
+
nscore.start_tempo.should be_a tempo_class
|
374
|
+
nscore.start_tempo.value.should eq(tgt_val)
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
it 'should use output from convert_program' do
|
379
|
+
prog = Program.new([0...4,2...5])
|
380
|
+
score = MeasureScore.new(FOUR_FOUR, Tempo::BPM.new(120), program: prog)
|
381
|
+
nscore = score.to_note_score(Tempo::QNPM)
|
382
|
+
nscore.program.should eq(score.convert_program)
|
383
|
+
end
|
384
|
+
|
385
|
+
it 'should use output from convert_parts' do
|
386
|
+
changeA = Change::Immediate.new(Dynamics::PP)
|
387
|
+
changeB = Change::Gradual.new(Dynamics::F, 2)
|
388
|
+
score = MeasureScore.new(FOUR_FOUR, Tempo::BPM.new(120),
|
389
|
+
parts: {"simple" => Part.new(Dynamics::MP, dynamic_changes: { 1 => changeA, 3 => changeB })}
|
390
|
+
)
|
391
|
+
nscore = score.to_note_score(Tempo::QNPM)
|
392
|
+
nscore.parts.should eq(score.convert_parts)
|
393
|
+
end
|
394
|
+
|
395
|
+
it 'should use output from convert_program' do
|
396
|
+
changeA = Change::Immediate.new(Dynamics::PP)
|
397
|
+
changeB = Change::Gradual.new(Dynamics::F, 2)
|
398
|
+
score = MeasureScore.new(FOUR_FOUR, Tempo::BPM.new(120),
|
399
|
+
parts: {"simple" => Part.new(Dynamics::MP, dynamic_changes: { 1 => changeA, 3 => changeB })}
|
400
|
+
)
|
401
|
+
nscore = score.to_note_score(Tempo::QNPM)
|
402
|
+
nscore.parts.should eq(score.convert_parts)
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Tempo::QNPM do
|
4
|
+
before :all do
|
5
|
+
@tempo = Tempo::QNPM.new(60)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe '#to_npm' do
|
9
|
+
it 'should return a Tempo::NPM object' do
|
10
|
+
@tempo.to_npm.should be_a Tempo::NPM
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should change tempo value to be 1/4th' do
|
14
|
+
@tempo.to_npm.value.should eq(Rational(60,4))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#to_nps' do
|
19
|
+
it 'should return a Tempo::NPS object' do
|
20
|
+
@tempo.to_nps.should be_a Tempo::NPS
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should change tempo value to be 1/240th' do
|
24
|
+
@tempo.to_nps.value.should eq(Rational(1,4))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#to_bpm' do
|
29
|
+
it 'should return a Tempo::BPM object' do
|
30
|
+
@tempo.to_bpm(Rational(1,4)).should be_a Tempo::BPM
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should divide tempo value by (4*beatdur)' do
|
34
|
+
@tempo.to_bpm(Rational(1,4)).value.should eq(60)
|
35
|
+
@tempo.to_bpm(Rational(1,2)).value.should eq(30)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe Tempo::NPM do
|
41
|
+
before :all do
|
42
|
+
@tempo = Tempo::NPM.new(60)
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#to_qnpm' do
|
46
|
+
it 'should return a Tempo::QNPM object' do
|
47
|
+
@tempo.to_qnpm.should be_a Tempo::QNPM
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should multiply tempo value by 4' do
|
51
|
+
@tempo.to_qnpm.value.should eq(240)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#to_nps' do
|
56
|
+
it 'should return a Tempo::NPS object' do
|
57
|
+
@tempo.to_nps.should be_a Tempo::NPS
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should change tempo value to be 1/60th' do
|
61
|
+
@tempo.to_nps.value.should eq(1)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe '#to_bpm' do
|
66
|
+
it 'should return a Tempo::BPM object' do
|
67
|
+
@tempo.to_bpm(Rational(1,4)).should be_a Tempo::BPM
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should divide tempo value by beatdur' do
|
71
|
+
@tempo.to_bpm(Rational(1,1)).value.should eq(60)
|
72
|
+
@tempo.to_bpm(Rational(1,4)).value.should eq(240)
|
73
|
+
@tempo.to_bpm(Rational(1,2)).value.should eq(120)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe Tempo::NPS do
|
79
|
+
before :all do
|
80
|
+
@tempo = Tempo::NPS.new(1)
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '#to_qnpm' do
|
84
|
+
it 'should return a Tempo::QNPM object' do
|
85
|
+
@tempo.to_qnpm.should be_a Tempo::QNPM
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should multiply tempo value by 240' do
|
89
|
+
@tempo.to_qnpm.value.should eq(240)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe '#to_npm' do
|
94
|
+
it 'should return a Tempo::NPM object' do
|
95
|
+
@tempo.to_npm.should be_a Tempo::NPM
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should multiply tempo value by 60' do
|
99
|
+
@tempo.to_npm.value.should eq(60)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe '#to_bpm' do
|
104
|
+
it 'should return a Tempo::BPM object' do
|
105
|
+
@tempo.to_bpm(Rational(1,4)).should be_a Tempo::BPM
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'should multiply tempo value by 60/beatdur' do
|
109
|
+
@tempo.to_bpm(Rational(1,1)).value.should eq(60)
|
110
|
+
@tempo.to_bpm(Rational(1,4)).value.should eq(240)
|
111
|
+
@tempo.to_bpm(Rational(1,2)).value.should eq(120)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe Tempo::BPM do
|
117
|
+
before :all do
|
118
|
+
@tempo = Tempo::BPM.new(60)
|
119
|
+
end
|
120
|
+
|
121
|
+
describe '#to_npm' do
|
122
|
+
it 'should return a Tempo::NPM object' do
|
123
|
+
@tempo.to_npm(Rational(1,4)).should be_a Tempo::NPM
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'should multiply tempo value by beatdur' do
|
127
|
+
@tempo.to_npm(Rational(1,4)).value.should eq(15)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe '#to_nps' do
|
132
|
+
it 'should return a Tempo::NPS object' do
|
133
|
+
@tempo.to_nps(Rational(1,4)).should be_a Tempo::NPS
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'should multiply tempo value by beatdur/60' do
|
137
|
+
@tempo.to_nps(Rational(1,4)).value.should eq(Rational(1,4))
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe '#to_qnpm' do
|
142
|
+
it 'should return a Tempo::QNPM object' do
|
143
|
+
@tempo.to_qnpm(Rational(1,4)).should be_a Tempo::QNPM
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'should multiply tempo value by (4*beatdur)' do
|
147
|
+
@tempo.to_qnpm(Rational(1,8)).value.should eq(30)
|
148
|
+
@tempo.to_qnpm(Rational(1,4)).value.should eq(60)
|
149
|
+
@tempo.to_qnpm(Rational(1,2)).value.should eq(120)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
data/spec/model/change_spec.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
2
|
|
3
3
|
describe Change::Immediate do
|
4
|
-
context '
|
4
|
+
context '#initialize' do
|
5
5
|
it 'should set value to given' do
|
6
6
|
Change::Immediate.new(5).value.should eq 5
|
7
7
|
end
|
@@ -29,6 +29,39 @@ describe Change::Immediate do
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
+
describe Change::Gradual do
|
33
|
+
context '#initialize' do
|
34
|
+
it 'should set value to given value' do
|
35
|
+
Change::Gradual.new(5,2).value.should eq 5
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should set duration to given duration' do
|
39
|
+
Change::Gradual.new(5,2).duration.should eq 2
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '==' do
|
44
|
+
it 'should return true if two gradual changes have the same value and duration' do
|
45
|
+
Change::Gradual.new(5,2).should eq(Change::Gradual.new(5,2))
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should return false if two gradual changes do not have the same value' do
|
49
|
+
Change::Gradual.new(5,2).should_not eq(Change::Gradual.new(4,2))
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should return false if two gradual changes do not have the same duration' do
|
53
|
+
Change::Gradual.new(5,2).should_not eq(Change::Gradual.new(5,1))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe '#to_yaml' do
|
58
|
+
it 'should produce YAML that can be loaded' do
|
59
|
+
c = Change::Gradual.new(4,2)
|
60
|
+
YAML.load(c.to_yaml).should eq c
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
32
65
|
describe Change::Gradual do
|
33
66
|
context '.new' do
|
34
67
|
it 'should set value to given value' do
|
@@ -60,4 +93,45 @@ describe Change::Gradual do
|
|
60
93
|
YAML.load(c.to_yaml).should eq c
|
61
94
|
end
|
62
95
|
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe Change::Partial do
|
99
|
+
context '#initialize' do
|
100
|
+
it 'should set value to given value' do
|
101
|
+
Change::Partial.new(200,5,1,2).value.should eq(200)
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'should set duration to given elapsed_dur - stop_dur' do
|
105
|
+
Change::Partial.new(200,5,1,2).duration.should eq(1)
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'should not raise NegativeError if given elapsed == 0' do
|
109
|
+
expect { Change::Partial.new(200,5,0,2) }.to_not raise_error
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'should raise NegativeError if given elapsed < 0' do
|
113
|
+
expect { Change::Partial.new(200,5,-1e-15,2) }.to raise_error(NegativeError)
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'should raise NonPositiveError if given stop <= 0' do
|
117
|
+
expect { Change::Partial.new(200,5,0,0) }.to raise_error(NonPositiveError)
|
118
|
+
expect { Change::Partial.new(200,5,0,-1) }.to raise_error(NonPositiveError)
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'should raise ArgumentError if given stop > total dur' do
|
122
|
+
expect { Change::Partial.new(200,5,1,5.001) }.to raise_error(ArgumentError)
|
123
|
+
expect { Change::Partial.new(200,5,1,10) }.to raise_error(ArgumentError)
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'should not raise ArgumentError if given stop == total dur' do
|
127
|
+
expect { Change::Partial.new(200,5,1,5) }.to_not raise_error
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'should raise ArgumentError if elapsed >= stop' do
|
131
|
+
expect { Change::Partial.new(200,5,1,1) }.to raise_error(ArgumentError)
|
132
|
+
expect { Change::Partial.new(200,5,3,2) }.to raise_error(ArgumentError)
|
133
|
+
expect { Change::Partial.new(200,5,5,5) }.to raise_error(ArgumentError)
|
134
|
+
expect { Change::Partial.new(200,5,6,5) }.to raise_error(ArgumentError)
|
135
|
+
end
|
136
|
+
end
|
63
137
|
end
|
@@ -70,6 +70,8 @@ describe MeasureScore do
|
|
70
70
|
:meter_changes => { 1 => Change::Immediate.new(5) } ],
|
71
71
|
'non-immediate meter change' => [ FOUR_FOUR, Tempo::BPM.new(120),
|
72
72
|
:meter_changes => { 1 => Change::Gradual.new(TWO_FOUR,1) } ],
|
73
|
+
'non-integer meter change offset' => [ FOUR_FOUR, Tempo::BPM.new(120),
|
74
|
+
:meter_changes => { 1.1 => Change::Immediate.new(TWO_FOUR) } ],
|
73
75
|
'tempo change value is not a Tempo object' => [ FOUR_FOUR, Tempo::QNPM.new(120),
|
74
76
|
:tempo_changes => { 1 => Change::Gradual.new(140,1) } ],
|
75
77
|
'invalid part' => [ FOUR_FOUR, 120, :parts => { "piano" => Part.new(-0.1) }],
|
data/spec/model/part_spec.rb
CHANGED
@@ -41,9 +41,6 @@ describe Part do
|
|
41
41
|
:dynamic_changes => { 0.2 => Change::Immediate.new(-0.01), 0.3 => Change::Gradual.new(1.01,0.2) }],
|
42
42
|
'notes with 0 duration' => [ 0.5, :notes => [ Note.new(0) ]],
|
43
43
|
'notes with negative duration' => [ 0.5, :notes => [ Note.new(-1) ]],
|
44
|
-
'gradual change with negative duration' => [
|
45
|
-
0.5, :notes => [ Note.new(1) ],
|
46
|
-
:dynamic_changes => { 0.2 => Change::Gradual.new(0.6,-0.1) }]
|
47
44
|
}.each do |context_str, args|
|
48
45
|
context context_str do
|
49
46
|
it 'should return false' do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: music-transcription
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.20.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- James Tunnell
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-11-
|
11
|
+
date: 2014-11-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -169,6 +169,9 @@ files:
|
|
169
169
|
- examples/song2.yml
|
170
170
|
- examples/song2_packed.yml
|
171
171
|
- lib/music-transcription.rb
|
172
|
+
- lib/music-transcription/conversion/measure_note_map.rb
|
173
|
+
- lib/music-transcription/conversion/measure_score_conversion.rb
|
174
|
+
- lib/music-transcription/conversion/tempo_conversion.rb
|
172
175
|
- lib/music-transcription/errors.rb
|
173
176
|
- lib/music-transcription/model/articulations.rb
|
174
177
|
- lib/music-transcription/model/change.rb
|
@@ -226,6 +229,9 @@ files:
|
|
226
229
|
- lib/music-transcription/validatable.rb
|
227
230
|
- lib/music-transcription/version.rb
|
228
231
|
- music-transcription.gemspec
|
232
|
+
- spec/conversion/measure_note_map_spec.rb
|
233
|
+
- spec/conversion/measure_score_conversion_spec.rb
|
234
|
+
- spec/conversion/tempo_conversion_spec.rb
|
229
235
|
- spec/model/change_spec.rb
|
230
236
|
- spec/model/link_spec.rb
|
231
237
|
- spec/model/measure_score_spec.rb
|
@@ -288,6 +294,9 @@ specification_version: 4
|
|
288
294
|
summary: Classes for representing music notational features like pitch, note, loudness,
|
289
295
|
tempo, etc.
|
290
296
|
test_files:
|
297
|
+
- spec/conversion/measure_note_map_spec.rb
|
298
|
+
- spec/conversion/measure_score_conversion_spec.rb
|
299
|
+
- spec/conversion/tempo_conversion_spec.rb
|
291
300
|
- spec/model/change_spec.rb
|
292
301
|
- spec/model/link_spec.rb
|
293
302
|
- spec/model/measure_score_spec.rb
|