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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f5b490ee5916154f2376f07568ccd234ff6fb82a
4
- data.tar.gz: 2a2fff2f3b7e94deacc2438d510f1d803cf6efcb
3
+ metadata.gz: 31245c605d780f4ff342ad6a33f9296a05a1dba1
4
+ data.tar.gz: 9be01f0138f712d589178163b62d71439f6e0c4f
5
5
  SHA512:
6
- metadata.gz: 797d3b962ded3291e92d8ba49a390072663d1ae37b0e7e957b3acc562d2d1793d569a7c117fd750de3bb99e03ed4d935cc0b13338c95e669993128acbd246902
7
- data.tar.gz: e0ae19e64aa43b0dc274d61601db32cbcccf87b33226c312e4f76407f7e87b7694a4f678c32de08bf17a714da5aa2e113a7ca15b470dbc2e8b48dbab07360489
6
+ metadata.gz: 80e8d7b289839c11bf75ef05e1786d1fa058c43e82f2ae67e3bae242954dca33fbd2eb2013664e481c41c5f1517a3a38664eabe45fdab187a63f7a6c30c0b917
7
+ data.tar.gz: dff4b97a87751abafebc8f95364b85b47d453773e838e4c3e896840d2a31b00e32ca6688e444c2a342b96e5b0d7385bba8d602e6055d7e52a2d05d86d5bcdb97
@@ -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
@@ -6,5 +6,6 @@ module Transcription
6
6
  class NonIntegerError < StandardError; end
7
7
  class NonRationalError < StandardError; end
8
8
  class NonIncreasingError < StandardError; end
9
+ class NotValidError < StandardError; end
9
10
  end
10
11
  end
@@ -2,7 +2,7 @@ module Music
2
2
  module Transcription
3
3
 
4
4
  class Change
5
- attr_accessor :value, :duration
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 check_methods
26
- [ :ensure_zero_duration ]
23
+ def clone
24
+ Immediate.new(@value)
27
25
  end
28
26
 
29
- def ensure_zero_duration
30
- unless @duration == 0
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
- include Validatable
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 initialize value, transition_duration
40
- super(value, transition_duration)
40
+ def clone
41
+ Gradual.new(@value,@duration)
41
42
  end
42
43
 
43
- def check_methods
44
- [ :ensure_nonnegative_duration ]
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 ensure_nonnegative_duration
48
- if @duration < 0
49
- raise NegativeError, "gradual change duration #{self.duration} must be non-negative"
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, program: program, parts: parts)
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, :check_meterchange_durs]
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
- super() + [ Tempo::BPM ]
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.duration != 0 }
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} have non-zero 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 ] + @tempo_changes.values + @parts.values
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
- unless valid_tempo_types.include?(@start_tempo.class)
32
- raise TypeError, "type of start tempo #{@start_tempo} is not one of valid tempo types: #{valid_tempo_types}"
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
- baddtypes = @tempo_changes.select {|k,v| !valid_tempo_types.include?(v.value.class) }
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: #{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
 
@@ -21,7 +21,7 @@ class Part
21
21
  end
22
22
 
23
23
  def validatables
24
- @notes + @dynamic_changes.values
24
+ @notes
25
25
  end
26
26
 
27
27
  def clone
@@ -12,15 +12,14 @@ class Tempo
12
12
  self.class == other.class && self.value == other.value
13
13
  end
14
14
 
15
- [ :qnpm, :bpm, :npm, :nps ].each do |sym|
16
- klass = Class.new(Tempo) do
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
@@ -2,6 +2,6 @@
2
2
  module Music
3
3
  module Transcription
4
4
  # music-transcription version
5
- VERSION = "0.19.0"
5
+ VERSION = "0.20.0"
6
6
  end
7
7
  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
@@ -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 '.new' do
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) }],
@@ -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.19.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 00:00:00.000000000 Z
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