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 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