music-transcription 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|