music-transcription 0.7.1 → 0.7.2

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: e410c49d0b013842573762574ed5406d7676843b
4
- data.tar.gz: 425be325e12b0908c57c1897ebad82443e685f81
3
+ metadata.gz: 47821dcba8bd951fa6052d201b49c4253bf6a8c0
4
+ data.tar.gz: fc84a5d2ea369f1d17fcdf52a82e8d9a61bef9da
5
5
  SHA512:
6
- metadata.gz: 6fffac146f3b86db14687f3bd1b7e348d883351a3d368f2a192142f36951e5ae8225bd13c98c5f8fd2c2773766b162354528d82e035a350e1c0c01b6bddc75d0
7
- data.tar.gz: 942d91f19af6e77d8ab61714bfe43e6c35d36e186dbd6adce002b131a6211e182c999e83a6472418d4e100156c7321569ecee6532f714e452ce4ec2f82fd544e
6
+ metadata.gz: 20284ea1e670062928ef8dc4ea86a342392e6746c7b511ced9ea2e4843a5b9705ffee13302ce984d09d12a01a32ccd2a23e4a25ae8787597afe2e97df3740928
7
+ data.tar.gz: 0c9083eb4e7cc5841db15f0f873f3f0835b27b403f1cd428d7536dd9113f5c630e41a7af306f6e2c468d82c4bfe47911681c4eff5c47d6aa69309ca0d05d710f
@@ -1,5 +1,7 @@
1
1
  # basic core classes
2
2
  require 'music-transcription/version'
3
+ require 'music-transcription/validatable'
4
+ require 'music-transcription/errors'
3
5
 
4
6
  # code for transcribing (representing) music
5
7
  require 'music-transcription/pitch'
@@ -9,11 +11,10 @@ require 'music-transcription/accent'
9
11
  require 'music-transcription/accents'
10
12
  require 'music-transcription/change'
11
13
  require 'music-transcription/note'
12
- require 'music-transcription/profile'
13
- require 'music-transcription/dynamic'
14
14
  require 'music-transcription/dynamics'
15
15
  require 'music-transcription/part'
16
16
  require 'music-transcription/program'
17
17
  require 'music-transcription/tempo'
18
18
  require 'music-transcription/meter'
19
+ require 'music-transcription/meters'
19
20
  require 'music-transcription/score'
@@ -7,10 +7,6 @@ class Change
7
7
  def initialize value, duration
8
8
  @value = value
9
9
  @duration = duration
10
-
11
- unless duration >= 0
12
- raise ArgumentError, "duration #{duration} must be >= 0"
13
- end
14
10
  end
15
11
 
16
12
  def ==(other)
@@ -20,15 +16,33 @@ class Change
20
16
  end
21
17
 
22
18
  class Immediate < Change
19
+ include Validatable
20
+
23
21
  def initialize value
22
+ @check_methods = [ :ensure_zero_duration ]
24
23
  super(value,0)
25
24
  end
25
+
26
+ def ensure_zero_duration
27
+ unless @duration == 0
28
+ raise ValueNotZeroError, "immediate change duration #{self.duration} must be 0"
29
+ end
30
+ end
26
31
  end
27
32
 
28
33
  class Gradual < Change
34
+ include Validatable
35
+
29
36
  def initialize value, transition_duration
37
+ @check_methods = [ :ensure_positive_duration ]
30
38
  super(value, transition_duration)
31
39
  end
40
+
41
+ def ensure_positive_duration
42
+ if @duration < 0
43
+ raise ValueNotPositiveError, "gradual change duration #{self.duration} must be >= 0"
44
+ end
45
+ end
32
46
  end
33
47
  end
34
48
 
@@ -1,14 +1,14 @@
1
1
  module Music
2
2
  module Transcription
3
- module Dynamics
4
- PPP = Dynamic::Pianississimo.new
5
- PP = Dynamic::Pianissimo.new
6
- P = Dynamic::Piano.new
7
- MP = Dynamic::MezzoPiano.new
8
- MF = Dynamic::MezzoForte.new
9
- F = Dynamic::Forte.new
10
- FF = Dynamic::Fortissimo.new
11
- FFF = Dynamic::Fortississimo.new
3
+ module Dynamics
4
+ PPP = 0.125
5
+ PP = 0.25
6
+ P = 0.375
7
+ MP = 0.5
8
+ MF = 0.625
9
+ F = 0.75
10
+ FF = 0.875
11
+ FFF = 1.0
12
12
  end
13
13
  end
14
14
  end
@@ -1,5 +1,10 @@
1
1
  module Music
2
2
  module Transcription
3
- class ValueNotPositiveError < RuntimeError;
4
- class ValueOutOfRangeError < RuntimeError;
3
+ class ValueNotPositiveError < StandardError; end
4
+ class ValueOutOfRangeError < StandardError; end
5
+ class ValueNotZeroError < StandardError; end
6
+ class NotPositiveIntegerError < StandardError; end
7
+ class SegmentNotIncreasingError < StandardError; end
8
+ class SegmentNegativeError < StandardError; end
5
9
  end
10
+ end
@@ -2,12 +2,27 @@ module Music
2
2
  module Transcription
3
3
 
4
4
  class Meter
5
+ include Validatable
5
6
 
6
7
  attr_reader :measure_duration, :beat_duration, :beats_per_measure
7
8
  def initialize beats_per_measure, beat_duration
8
9
  @beats_per_measure = beats_per_measure
9
10
  @beat_duration = beat_duration
10
11
  @measure_duration = beats_per_measure * beat_duration
12
+
13
+ @check_methods = [ :check_beats_per_measure, :check_beat_duration ]
14
+ end
15
+
16
+ def check_beats_per_measure
17
+ unless @beats_per_measure.is_a?(Integer) && @beats_per_measure > 0
18
+ raise NotPositiveIntegerError, "beats per measure #{@beats_per_measure} is not a positive integer"
19
+ end
20
+ end
21
+
22
+ def check_beat_duration
23
+ unless @beat_duration > 0
24
+ raise ValueNotPositiveError, "beat duration #{@beat_duration} is not positive"
25
+ end
11
26
  end
12
27
 
13
28
  def ==(other)
@@ -0,0 +1,11 @@
1
+ module Music
2
+ module Transcription
3
+ module Meters
4
+ TWO_TWO = Meter.new(2,"1/2".to_r)
5
+ TWO_FOUR = Meter.new(2,"1/4".to_r)
6
+ THREE_FOUR = Meter.new(3,"1/4".to_r)
7
+ FOUR_FOUR = Meter.new(4,"1/4".to_r)
8
+ SIX_EIGHT = Meter.new(6,"1/8".to_r)
9
+ end
10
+ end
11
+ end
@@ -4,14 +4,23 @@ module Transcription
4
4
  require 'set'
5
5
 
6
6
  class Note
7
- attr_reader :duration, :pitches, :links
8
- attr_accessor :accent
7
+ include Validatable
8
+
9
+ attr_reader :pitches, :links
10
+ attr_accessor :accent, :duration
9
11
 
10
12
  def initialize duration, pitches = [], links: {}, accent: Accents::NONE
11
13
  self.duration = duration
12
14
  @pitches = Set.new(pitches).sort
13
15
  @links = links
14
- self.accent = accent
16
+ @duration = duration
17
+ @accent = accent
18
+
19
+ @check_methods = [ :ensure_positive_duration ]
20
+ end
21
+
22
+ def ensure_positive_duration
23
+ raise ValueNotPositiveError, "duration #{@duration} is not positive" if @duration <= 0
15
24
  end
16
25
 
17
26
  def == other
@@ -20,14 +29,6 @@ class Note
20
29
  (@links.to_a.sort == other.links.to_a.sort) &&
21
30
  (@accent == other.accent)
22
31
  end
23
-
24
- # Set the note duration.
25
- # @param [Numeric] duration The duration to use.
26
- # @raise [ArgumentError] if duration is not greater than 0.
27
- def duration= duration
28
- raise ValueNotPositiveError if duration <= 0
29
- @duration = duration
30
- end
31
32
 
32
33
  def clone
33
34
  Marshal.load(Marshal.dump(self))
@@ -61,6 +62,10 @@ class Note
61
62
  return self
62
63
  end
63
64
 
65
+ def valid?
66
+ @duration > 0
67
+ end
68
+
64
69
  class Sixteenth < Note
65
70
  def initialize pitches = [], links: {}, accent: Accents::NONE
66
71
  super(Rational(1,16),pitches,links:links,accent:accent)
@@ -4,6 +4,8 @@ module Music
4
4
  module Transcription
5
5
 
6
6
  class Part
7
+ include Validatable
8
+
7
9
  attr_reader :start_dynamic, :dynamic_changes, :notes
8
10
 
9
11
  def initialize start_dynamic, notes: [], dynamic_changes: {}
@@ -11,11 +13,13 @@ class Part
11
13
  @start_dynamic = start_dynamic
12
14
  @dynamic_changes = dynamic_changes
13
15
 
14
- d = self.duration
15
- badkeys = dynamic_changes.keys.select {|k| k < 0 || k > d }
16
- if badkeys.any?
17
- raise ArgumentError, "dynamic profile has changes outside 0..d"
18
- end
16
+ @check_methods = [:ensure_start_dynamic, :ensure_dynamic_change_values_range
17
+ #:ensure_dynamic_change_offsets
18
+ ]
19
+ end
20
+
21
+ def validatables
22
+ @notes + @dynamic_changes.values
19
23
  end
20
24
 
21
25
  def clone
@@ -31,6 +35,27 @@ class Part
31
35
  def duration
32
36
  return @notes.inject(0) { |sum, note| sum + note.duration }
33
37
  end
38
+
39
+ def ensure_start_dynamic
40
+ unless @start_dynamic.between?(0,1)
41
+ raise RangeError, "start dynamic #{@start_dynamic} is not between 0 and 1"
42
+ end
43
+ end
44
+
45
+ #def ensure_dynamic_change_offsets
46
+ # d = self.duration
47
+ # outofrange = @dynamic_changes.keys.select {|k| !k.between?(0,d) }
48
+ # if outofrange.any?
49
+ # raise RangeError, "dynamic change offsets #{outofrange} are not between 0 and #{d}"
50
+ # end
51
+ #end
52
+
53
+ def ensure_dynamic_change_values_range
54
+ outofrange = @dynamic_changes.values.select {|v| !v.value.between?(0,1) }
55
+ if outofrange.any?
56
+ raise RangeError, "dynamic change values #{outofrange} are not between 0 and 1"
57
+ end
58
+ end
34
59
  end
35
60
 
36
61
  end
@@ -6,12 +6,15 @@ module Transcription
6
6
  # @author James Tunnell
7
7
  #
8
8
  class Program
9
+ include Validatable
10
+
9
11
  attr_accessor :segments
10
12
 
11
13
  # A new instance of Program.
12
14
  # @param [Hash] args Hashed arguments. Required key is :segments.
13
15
  def initialize segments = []
14
16
  @segments = segments
17
+ @check_methods = [:ensure_increasing_segments, :ensure_nonnegative_segments]
15
18
  end
16
19
 
17
20
  # @return [Float] the sum of all program segment lengths
@@ -21,8 +24,7 @@ class Program
21
24
 
22
25
  # compare to another Program
23
26
  def == other
24
- # raise ArgumentError, "program is invalid" if !self.valid?
25
- return @segments == other.segments
27
+ return other.respond_to?(:segments) && @segments == other.segments
26
28
  end
27
29
 
28
30
  def include? offset
@@ -33,74 +35,20 @@ class Program
33
35
  end
34
36
  return false
35
37
  end
36
-
37
- # For the given note elapsed, what will the note offset be?
38
- #
39
- def note_offset_for elapsed
40
- raise ArgumentError, "elapsed #{elapsed} is less than 0.0" if elapsed < 0.0
41
- raise ArgumentError, "elapsed #{elapsed} is greater than program length" if elapsed > self.length
42
-
43
- so_far = 0.0
44
-
45
- @segments.each do |segment|
46
- segment_length = segment.last - segment.first
47
-
48
- if (segment_length + so_far) > elapsed
49
- return segment.first + (elapsed - so_far)
50
- else
51
- so_far += segment_length
52
- end
53
- end
54
-
55
- raise "offset not determined even though the given elapsed is less than program length!"
56
- end
57
38
 
58
- # For the given note offset in the score, how much note will have elapsed to
59
- # get there according to the program?
60
- #
61
- def note_elapsed_at offset
62
- raise ArgumentError, "offset #{offset} is not included in program" if !self.include?(offset)
63
-
64
- elapsed = 0.0
65
-
66
- @segments.each do |segment|
67
- if segment.include?(offset)
68
- elapsed += (offset - segment.first)
69
- break
70
- else
71
- elapsed += (segment.last - segment.first)
72
- end
39
+ def ensure_increasing_segments
40
+ non_increasing = @segments.select {|seg| seg.first >= seg.last }
41
+ if non_increasing.any?
42
+ raise SegmentNotIncreasingError, "Non-increasing segments found #{non_increasing}"
73
43
  end
74
-
75
- return elapsed
76
44
  end
77
45
 
78
- # For the given note offset in the score, how much time will have elapsed to
79
- # get there according to the program?
80
- #
81
- def time_elapsed_at offset, note_time_converter
82
- raise ArgumentError, "offset #{offset} is not included in program" if !self.include?(offset)
83
-
84
- elapsed = 0.0
85
-
86
- @segments.each do |segment|
87
- if segment.include?(offset)
88
- elapsed += note_time_converter.time_elapsed(segment.first, offset)
89
- break
90
- else
91
- elapsed += note_time_converter.time_elapsed(segment.first, segment.last)
92
- end
46
+ def ensure_nonnegative_segments
47
+ negative = @segments.select {|seg| seg.first < 0 || seg.last < 0 }
48
+ if negative.any?
49
+ raise SegmentNegativeError, "Negative segments found #{negative}"
93
50
  end
94
-
95
- return elapsed
96
51
  end
97
-
98
- end
99
-
100
- module_function
101
-
102
- def program segments
103
- Program.new(:segments => segments)
104
52
  end
105
53
 
106
54
  end
@@ -2,6 +2,8 @@ module Music
2
2
  module Transcription
3
3
 
4
4
  class Score
5
+ include Validatable
6
+
5
7
  attr_reader :start_meter, :start_tempo, :parts, :program, :meter_changes, :tempo_changes
6
8
 
7
9
  def initialize start_meter, start_tempo, meter_changes: {}, tempo_changes: {}, parts: {}, program: Program.new
@@ -11,6 +13,54 @@ class Score
11
13
  @tempo_changes = tempo_changes
12
14
  @parts = parts
13
15
  @program = program
16
+
17
+ @check_methods = [ :check_start_tempo, :check_tempo_changes,
18
+ #:check_tempo_change_offsets, :check_meter_change_offsets,
19
+ :check_meter_changes ]
20
+ end
21
+
22
+ def validatables
23
+ return [ @program, @start_meter ] +
24
+ @tempo_changes.values +
25
+ @meter_changes.values +
26
+ @meter_changes.values.map {|v| v.value} +
27
+ @parts.values
28
+ end
29
+
30
+ def check_start_tempo
31
+ unless @start_tempo > 0
32
+ raise ValueNotPositiveError, "start tempo #{@start_tempo} is not positive"
33
+ end
34
+ end
35
+
36
+ def check_tempo_changes
37
+ negative = @tempo_changes.select {|k,v| v.value <= 0}
38
+ if negative.any?
39
+ raise ValueNotPositiveError, "tempo changes #{negative} are not positive"
40
+ end
41
+ end
42
+
43
+ #def check_tempo_change_offsets
44
+ # d = self.duration
45
+ # outofrange = @tempo_changes.keys.select {|k| !k.between?(0,d) }
46
+ # if outofrange.any?
47
+ # raise RangeError, "tempo change offsets #{outofrange} are not between 0 and #{d}"
48
+ # end
49
+ #end
50
+ #
51
+ #def check_meter_change_offsets
52
+ # d = self.duration
53
+ # outofrange = @meter_changes.keys.select {|k| !k.between?(0,d) }
54
+ # if outofrange.any?
55
+ # raise RangeError, "meter change offsets #{outofrange} are not between 0 and #{d}"
56
+ # end
57
+ #end
58
+
59
+ def check_meter_changes
60
+ nonzero_duration = @meter_changes.select {|k,v| v.duration != 0 }
61
+ if nonzero_duration.any?
62
+ raise ValueNotZeroError, "meter changes #{nonzero_duration} have non-zero duration"
63
+ end
14
64
  end
15
65
 
16
66
  def clone
@@ -28,7 +78,7 @@ class Score
28
78
 
29
79
  def duration
30
80
  @parts.map {|p| p.duration }.max
31
- end
81
+ end
32
82
  end
33
83
 
34
84
  end
@@ -0,0 +1,31 @@
1
+ # assumes that @checks is defined as an array of no-arg lambdas, each
2
+ # lambda raising an error (with useful msg) when check fails
3
+ module Validatable
4
+ attr_reader :errors
5
+
6
+ def validate
7
+ @errors = []
8
+ @check_methods.each do |check_method|
9
+ begin
10
+ send(check_method)
11
+ rescue StandardError => e
12
+ @errors.push e
13
+ end
14
+ end
15
+ if respond_to?(:validatables)
16
+ validatables.each do |v|
17
+ @errors += v.validate
18
+ end
19
+ end
20
+ return @errors
21
+ end
22
+
23
+ def valid?
24
+ self.validate
25
+ @errors.empty?
26
+ end
27
+
28
+ def invalid?
29
+ !self.valid?
30
+ end
31
+ end
@@ -2,6 +2,6 @@
2
2
  module Music
3
3
  module Transcription
4
4
  # music-transcription version
5
- VERSION = "0.7.1"
5
+ VERSION = "0.7.2"
6
6
  end
7
7
  end
data/spec/meter_spec.rb CHANGED
@@ -47,4 +47,33 @@ describe Meter do
47
47
  YAML.load(m.to_yaml).should eq m
48
48
  end
49
49
  end
50
+
51
+ describe '#valid?' do
52
+ {
53
+ '4/4 meter' => [4,'1/4'.to_r],
54
+ '2/4 meter' => [2,'1/4'.to_r],
55
+ '3/4 meter' => [2,'1/4'.to_r],
56
+ '6/8 meter' => [6,'1/8'.to_r],
57
+ '12/8 meter' => [12,'1/8'.to_r],
58
+ }.each do |context_str,args|
59
+ context context_str do
60
+ it 'should return true' do
61
+ Score.new(*args).should be_valid
62
+ end
63
+ end
64
+ end
65
+
66
+ {
67
+ 'non-integer positive beats per measure' => [4.0,"1/4".to_r],
68
+ 'integer negative beats per measure' => [-1,"1/4".to_r],
69
+ 'zero beat duration' => [4,0.to_r],
70
+ 'negative beat duration' => [4,-1.to_r],
71
+ }.each do |context_str,args|
72
+ context context_str do
73
+ it 'should return false' do
74
+ Score.new(*args).should be_invalid
75
+ end
76
+ end
77
+ end
78
+ end
50
79
  end
data/spec/note_spec.rb CHANGED
@@ -102,4 +102,26 @@ describe Note do
102
102
  YAML.load(n.to_yaml).should eq n
103
103
  end
104
104
  end
105
+
106
+ describe '#valid?' do
107
+ context 'note with positive duration' do
108
+ it 'should return true' do
109
+ Note.new(1,[C2]).should be_valid
110
+ end
111
+ end
112
+
113
+ context 'note with 0 duration' do
114
+ it 'should return false' do
115
+ Note.new(0,[C2]).should be_invalid
116
+ require 'pry'
117
+ binding.pry
118
+ end
119
+ end
120
+
121
+ context 'note with negative duration' do
122
+ it 'should be invalid' do
123
+ Note.new(-1,[C2]).should be_invalid
124
+ end
125
+ end
126
+ end
105
127
  end
data/spec/part_spec.rb CHANGED
@@ -26,4 +26,47 @@ describe Part do
26
26
  YAML.load(p.to_yaml).should eq p
27
27
  end
28
28
  end
29
+
30
+ describe '#valid?' do
31
+ { 'negative start dynamic' => [-0.01],
32
+ 'start dynamic > 1' => [1.01],
33
+ #'dynamic change offsets outside 0..d' => [
34
+ # 0.5, :notes => [ Note::Whole.new ],
35
+ # :dynamic_changes => { 1.2 => Change::Immediate.new(0.5) }],
36
+ #'dynamic change offsets outside 0..d' => [
37
+ # 0.5, :notes => [ Note::Whole.new ],
38
+ # :dynamic_changes => { -0.2 => Change::Immediate.new(0.5) }],
39
+ 'dynamic change values outside 0..1' => [
40
+ 0.5, :notes => [ Note::Whole.new ],
41
+ :dynamic_changes => { 0.2 => Change::Immediate.new(-0.01), 0.3 => Change::Gradual.new(1.01,0.2) }],
42
+ 'notes with 0 duration' => [ 0.5, :notes => [ Note.new(0) ]],
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
+ }.each do |context_str, args|
48
+ context context_str do
49
+ it 'should return false' do
50
+ Part.new(*args).should be_invalid
51
+ end
52
+ end
53
+ end
54
+
55
+ {
56
+ 'valid notes' => [ Dynamics::PP,
57
+ :notes => [ Note::Whole.new, Note::Quarter.new([C5]) ]],
58
+ 'valid dynamic values' => [ Dynamics::MF,
59
+ :notes => [ Note::Whole.new([C4]), Note::Quarter.new ],
60
+ :dynamic_changes => {
61
+ 0.5 => Change::Immediate.new(Dynamics::MP),
62
+ 1.2 => Change::Gradual.new(Dynamics::FF, 0.05) } ],
63
+ }.each do |context_str, args|
64
+ context context_str do
65
+ it 'should return true' do
66
+ part = Part.new(*args)
67
+ part.should be_valid
68
+ end
69
+ end
70
+ end
71
+ end
29
72
  end
data/spec/program_spec.rb CHANGED
@@ -28,28 +28,23 @@ describe Program do
28
28
  end
29
29
  end
30
30
 
31
- describe "#note_elapsed_at" do
32
- before :each do
33
- segments = [ 0.0...5.0, 0.0...4.0, 5.0..10.0 ]
34
- @program = Program.new segments
35
- end
36
-
37
- it "should return 0.0 at program start" do
38
- @program.note_elapsed_at(@program.segments.first.first).should eq(0.0)
39
- end
40
-
41
- it "should return program length at program stop" do
42
- @program.note_elapsed_at(@program.segments.last.last).should eq(@program.length)
31
+ describe '#valid?' do
32
+ context 'increasing, positive segments' do
33
+ it 'should return true' do
34
+ Program.new([0..2,1..2,0..4]).should be_valid
35
+ end
43
36
  end
44
-
45
- it "should return correct note elapsed for any included offset" do
46
- @program.note_elapsed_at(2.5).should eq(2.5)
47
- @program.note_elapsed_at(5.5).should eq(9.5)
37
+
38
+ context 'decreasing, positive segments' do
39
+ it 'should return false' do
40
+ Program.new([2..0,2..1,04..0]).should be_invalid
41
+ end
48
42
  end
49
43
 
50
- it "should raise error if offset is not included" do
51
- lambda { @program.note_elapsed_at(-0.000001) }.should raise_error
52
- lambda { @program.note_elapsed_at(10.000001) }.should raise_error
44
+ context 'increasing, negative segments' do
45
+ it 'should return false' do
46
+ Program.new([-1..2,-2..0,-2..2]).should be_invalid
47
+ end
53
48
  end
54
49
  end
55
50
  end
data/spec/score_spec.rb CHANGED
@@ -3,20 +3,20 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
3
  describe Score do
4
4
  describe '#initialize' do
5
5
  it 'should use empty containers for parameters not given' do
6
- s = Score.new(Meter.new(4,4),120)
6
+ s = Score.new(FOUR_FOUR,120)
7
7
  s.parts.should be_empty
8
8
  s.program.segments.should be_empty
9
9
  end
10
10
 
11
11
  it 'should assign given parameters' do
12
- m = Meter.new(4,"1/4".to_r)
12
+ m = FOUR_FOUR
13
13
  s = Score.new(m,120)
14
14
  s.start_meter.should eq m
15
15
  s.start_tempo.should eq 120
16
16
 
17
17
  parts = { "piano (LH)" => Samples::SAMPLE_PART }
18
18
  program = Program.new [0...0.75, 0...0.75]
19
- mcs = { 1 => Change::Immediate.new(Meter.new(3,"1/4".to_r)) }
19
+ mcs = { 1 => Change::Immediate.new(THREE_FOUR) }
20
20
  tcs = { 1 => Change::Immediate.new(100) }
21
21
 
22
22
  s = Score.new(m,120,
@@ -31,4 +31,39 @@ describe Score do
31
31
  s.tempo_changes.should eq tcs
32
32
  end
33
33
  end
34
+
35
+ describe '#valid?' do
36
+ {
37
+ 'just valid start meter and tempo' => [ FOUR_FOUR, 120 ],
38
+ 'valid meter changes' => [ FOUR_FOUR, 120,
39
+ :meter_changes => { 1 => Change::Immediate.new(TWO_FOUR) } ],
40
+ 'valid tempo changes' => [ FOUR_FOUR, 120,
41
+ :tempo_changes => { 1 => Change::Gradual.new(200, 2), 2 => Change::Immediate.new(300) } ],
42
+ 'valid part' => [ FOUR_FOUR, 120, :parts => { "piano" => Samples::SAMPLE_PART }],
43
+ 'valid program' => [ FOUR_FOUR, 120, :program => Program.new([0..2,0..2]) ]
44
+ }.each do |context_str,args|
45
+ context context_str do
46
+ it 'should return true' do
47
+ Score.new(*args).should be_valid
48
+ end
49
+ end
50
+ end
51
+
52
+ {
53
+ 'invalid start tempo' => [ FOUR_FOUR, -1],
54
+ 'invalid start meter' => [ Meter.new(-1,"1/4".to_r), 120],
55
+ 'invalid meter in change' => [ FOUR_FOUR, 120,
56
+ :meter_changes => { 1 => Change::Immediate.new(Meter.new(-2,"1/4".to_r)) } ],
57
+ 'non-immediate meter change' => [ FOUR_FOUR, 120,
58
+ :meter_changes => { 1 => Change::Gradual.new(TWO_FOUR,1) } ],
59
+ 'invalid part' => [ FOUR_FOUR, 120, :parts => { "piano" => Part.new(-0.1) }],
60
+ 'invalid program' => [ FOUR_FOUR, 120, :program => Program.new([2..0]) ],
61
+ }.each do |context_str,args|
62
+ context context_str do
63
+ it 'should return false' do
64
+ Score.new(*args).should be_invalid
65
+ end
66
+ end
67
+ end
68
+ end
34
69
  end
data/spec/spec_helper.rb CHANGED
@@ -2,7 +2,8 @@ require 'rspec'
2
2
  require 'music-transcription'
3
3
 
4
4
  include Music::Transcription
5
- include Music::Transcription::Pitches
5
+ include Pitches
6
+ include Meters
6
7
 
7
8
  class Samples
8
9
  SAMPLE_PART = Part.new(
@@ -14,4 +15,16 @@ class Samples
14
15
  ],
15
16
  dynamic_changes: {1.0 => Change::Immediate.new(Dynamics::MP)}
16
17
  )
17
- end
18
+ end
19
+
20
+ RSpec::Matchers.define :be_valid do
21
+ match do |obj|
22
+ obj.valid?
23
+ end
24
+ end
25
+
26
+ RSpec::Matchers.define :be_invalid do
27
+ match do |obj|
28
+ obj.invalid?
29
+ end
30
+ end
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.7.1
4
+ version: 0.7.2
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-09-20 00:00:00.000000000 Z
11
+ date: 2014-09-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -154,19 +154,19 @@ files:
154
154
  - lib/music-transcription/accent.rb
155
155
  - lib/music-transcription/accents.rb
156
156
  - lib/music-transcription/change.rb
157
- - lib/music-transcription/dynamic.rb
158
157
  - lib/music-transcription/dynamics.rb
159
158
  - lib/music-transcription/errors.rb
160
159
  - lib/music-transcription/link.rb
161
160
  - lib/music-transcription/meter.rb
161
+ - lib/music-transcription/meters.rb
162
162
  - lib/music-transcription/note.rb
163
163
  - lib/music-transcription/part.rb
164
164
  - lib/music-transcription/pitch.rb
165
165
  - lib/music-transcription/pitches.rb
166
- - lib/music-transcription/profile.rb
167
166
  - lib/music-transcription/program.rb
168
167
  - lib/music-transcription/score.rb
169
168
  - lib/music-transcription/tempo.rb
169
+ - lib/music-transcription/validatable.rb
170
170
  - lib/music-transcription/version.rb
171
171
  - music-transcription.gemspec
172
172
  - spec/change_spec.rb
@@ -176,7 +176,6 @@ files:
176
176
  - spec/note_spec.rb
177
177
  - spec/part_spec.rb
178
178
  - spec/pitch_spec.rb
179
- - spec/profile_spec.rb
180
179
  - spec/program_spec.rb
181
180
  - spec/score_spec.rb
182
181
  - spec/spec_helper.rb
@@ -214,7 +213,6 @@ test_files:
214
213
  - spec/note_spec.rb
215
214
  - spec/part_spec.rb
216
215
  - spec/pitch_spec.rb
217
- - spec/profile_spec.rb
218
216
  - spec/program_spec.rb
219
217
  - spec/score_spec.rb
220
218
  - spec/spec_helper.rb
@@ -1,27 +0,0 @@
1
- module Music
2
- module Transcription
3
-
4
- # Defines a dynamic level
5
- #
6
- # @author James Tunnell
7
- #
8
- class Dynamic
9
- def ==(other)
10
- self.class == other.class
11
- end
12
-
13
- def clone
14
- self.class.new
15
- end
16
-
17
- [
18
- :Piano, :Pianissimo, :Pianississimo,
19
- :MezzoPiano, :MezzoForte,
20
- :Forte, :Fortissimo, :Fortississimo
21
- ].each do |name|
22
- Dynamic.const_set(name, Class.new(Dynamic))
23
- end
24
- end
25
-
26
- end
27
- end
@@ -1,148 +0,0 @@
1
- module Music
2
- module Transcription
3
-
4
- # Represent a setting that can change over time.
5
- #
6
- # @author James Tunnell
7
- #
8
- class Profile
9
- attr_accessor :start_value, :value_changes
10
-
11
- def initialize start_value, value_changes = {}
12
- @start_value = start_value
13
- @value_changes = value_changes
14
- end
15
-
16
- # Compare to another Profile object.
17
- def == other
18
- (self.start_value == other.start_value) &&
19
- (self.value_changes == other.value_changes)
20
- end
21
-
22
- # Produce an identical Profile object.
23
- def clone
24
- Marshal.load(Marshal.dump(self))
25
- end
26
-
27
- def last_value
28
- if @value_changes.empty?
29
- return @start_value
30
- else
31
- return @value_changes[@value_changes.keys.max].value
32
- end
33
- end
34
-
35
- def changes_before? offset
36
- @value_changes.count {|k,v| k < offset } > 0
37
- end
38
-
39
- def changes_after? offset
40
- @value_changes.count {|k,v| k > offset } > 0
41
- end
42
-
43
- # move changes forward or back by some offset
44
- def shift amt
45
- self.clone.shift! amt
46
- end
47
-
48
- # move changes forward or back by some offset
49
- def shift! amt
50
- @value_changes = Hash[@value_changes.map {|k,v| [k+amt,v]}]
51
- return self
52
- end
53
-
54
- def stretch ratio
55
- self.clone.stretch! ratio
56
- end
57
-
58
- def stretch! ratio
59
- @value_changes = Hash[ @value_changes.map {|k,v| [k*ratio,v] }]
60
- return self
61
- end
62
-
63
- def append profile, offset
64
- self.clone.append! profile
65
- end
66
-
67
- def append! profile, start_offset
68
- if @value_changes.any? && start_offset < @value_changes.keys.max
69
- raise ArgumentError, "appending profile overlaps"
70
- end
71
-
72
- lv = self.last_value
73
- unless lv == profile.start_value
74
- @value_changes[start_offset] = Change::Immediate.new(profile.start_value)
75
- lv = profile.start_value
76
- end
77
-
78
- shifted = profile.shift(start_offset)
79
- shifted.value_changes.sort.each do |offset,value_change|
80
- unless value_change.value == lv
81
- @value_changes[offset] = value_change
82
- lv = value_change.value
83
- end
84
- end
85
- return self
86
- end
87
-
88
- # Returns true if start value and value changes all are between given A and B.
89
- def values_between? a, b
90
- is_ok = self.start_value.between?(a,b)
91
-
92
- if is_ok
93
- self.value_changes.each do |offset, setting|
94
- setting.value.between?(a,b)
95
- end
96
- end
97
- return is_ok
98
- end
99
-
100
- # Returns true if start value and value changes all are greater than zero.
101
- def values_positive?
102
- is_ok = self.start_value > 0.0
103
-
104
- if is_ok
105
- self.value_changes.each do |offset, setting|
106
- setting.value > 0.0
107
- end
108
- end
109
- return is_ok
110
- end
111
-
112
- def clone_and_collate computer_class, program_segments
113
- new_profile = Profile.new start_value
114
-
115
- segment_start_offset = 0.0
116
- comp = computer_class.new(self)
117
-
118
- program_segments.each do |seg|
119
- # figure which dynamics to keep/modify
120
- changes = Marshal.load(Marshal.dump(value_changes))
121
- changes.keep_if {|offset,change| seg.include?(offset) }
122
- changes.each do |offset, change|
123
- if(offset + change.transition.duration) > seg.last
124
- change.transition.duration = seg.last - offset
125
- change.value = comp.value_at seg.last
126
- end
127
- end
128
-
129
- # find & add segment start value first
130
- value = comp.value_at seg.first
131
- offset = segment_start_offset
132
- new_profile.value_changes[offset] = value_change(value)
133
-
134
- # add changes to part, adjusting for segment start offset
135
- changes.each do |offset2, change|
136
- offset3 = (offset2 - seg.first) + segment_start_offset
137
- new_profile.value_changes[offset3] = change
138
- end
139
-
140
- segment_start_offset += (seg.last - seg.first)
141
- end
142
-
143
- return new_profile
144
- end
145
- end
146
-
147
- end
148
- end
data/spec/profile_spec.rb DELETED
@@ -1,163 +0,0 @@
1
- require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
-
3
- describe Profile do
4
-
5
- context '.new' do
6
- it "should assign start value given during construction" do
7
- p = Profile.new(0.2)
8
- p.start_value.should eq(0.2)
9
- end
10
-
11
- it "should assign settings given during construction" do
12
- p = Profile.new(0.2,
13
- 1.0 => Change::Immediate.new(0.5),
14
- 1.5 => Change::Immediate.new(1.0)
15
- )
16
- p.value_changes[1.0].value.should eq(0.5)
17
- p.value_changes[1.5].value.should eq(1.0)
18
- end
19
- end
20
-
21
- describe '#last_value' do
22
- context 'no value changes' do
23
- it 'should return the start value' do
24
- p = Profile.new(0.5)
25
- p.last_value.should eq 0.5
26
- end
27
- end
28
-
29
- context 'with value changes' do
30
- it 'should return the value with highest key' do
31
- p = Profile.new(0.5, 1.0 => Change::Immediate.new(0.6), 2.0 => Change::Immediate.new(0.7))
32
- p.last_value.should eq 0.7
33
- end
34
- end
35
- end
36
-
37
- describe '#changes_before?' do
38
- context 'no value changes' do
39
- it 'should return false' do
40
- p = Profile.new(0.0)
41
- p.changes_before?(0).should be false
42
- p.changes_before?(-10000000000000).should be false
43
- p.changes_before?(10000000000000).should be false
44
- end
45
- end
46
-
47
- context 'with value changes' do
48
- context 'with changes before given offset' do
49
- it 'should return true' do
50
- p = Profile.new(0.0, 5.0 => Change::Immediate.new(0.1), 7.5 => Change::Immediate.new(0.2))
51
- p.changes_before?(10.0).should be true
52
- end
53
- end
54
-
55
- context 'with no changes before given offset' do
56
- it 'should return false' do
57
- p = Profile.new(0.0, 5.0 => Change::Immediate.new(0.1), 7.5 => Change::Immediate.new(0.2))
58
- p.changes_before?(5.0).should be false
59
- end
60
- end
61
- end
62
- end
63
-
64
- describe '#changes_after?' do
65
- context 'no value changes' do
66
- it 'should return false' do
67
- p = Profile.new(0.0)
68
- p.changes_after?(0).should be false
69
- p.changes_after?(-10000000000000).should be false
70
- p.changes_after?(10000000000000).should be false
71
- end
72
- end
73
-
74
- context 'with value changes' do
75
- context 'with changes after given offset' do
76
- it 'should return true' do
77
- p = Profile.new(0.0, 5.0 => Change::Immediate.new(0.1), 7.5 => Change::Immediate.new(0.2))
78
- p.changes_after?(0.0).should be true
79
- end
80
- end
81
-
82
- context 'with no changes after given offset' do
83
- it 'should return false' do
84
- p = Profile.new(0.0, 5.0 => Change::Immediate.new(0.1), 7.5 => Change::Immediate.new(0.2))
85
- p.changes_after?(7.5).should be false
86
- end
87
- end
88
- end
89
- end
90
-
91
- describe '#shift!' do
92
- it 'should add shift amount to all change offsets' do
93
- p = Profile.new(0.0, 5.0 => Change::Immediate.new(0.1), 7.5 => Change::Immediate.new(0.2))
94
- p.shift!(1.0)
95
- p.value_changes[6.0].value.should eq(0.1)
96
- p.value_changes[8.5].value.should eq(0.2)
97
- p.shift!(-1.0)
98
- p.value_changes[5.0].value.should eq(0.1)
99
- p.value_changes[7.5].value.should eq(0.2)
100
- end
101
- end
102
-
103
- describe '#stretch!' do
104
- it 'should multiply change offsets by ratio' do
105
- p = Profile.new(0.0, 5.0 => Change::Immediate.new(0.1), 7.5 => Change::Immediate.new(0.2))
106
- p.stretch!(1)
107
- p.value_changes[5.0].value.should eq(0.1)
108
- p.value_changes[7.5].value.should eq(0.2)
109
- p.stretch!("3/2".to_r)
110
- p.value_changes[7.5].value.should eq(0.1)
111
- p.value_changes[11.25].value.should eq(0.2)
112
- p.stretch!("2/3".to_r)
113
- p.value_changes[5.0].value.should eq(0.1)
114
- p.value_changes[7.5].value.should eq(0.2)
115
- end
116
- end
117
-
118
- describe '#append!' do
119
- before :each do
120
- @p1 = Profile.new(0.0, 5.0 => Change::Immediate.new(0.1), 7.5 => Change::Immediate.new(0.2))
121
- @p2 = Profile.new(0.2, 1.0 => Change::Immediate.new(0.0), 2.0 => Change::Gradual.new(100.0,1.0))
122
- @p3 = Profile.new(0.1, 1.0 => Change::Immediate.new(0.0))
123
- end
124
-
125
- context 'offset less than last value change offset' do
126
- it' should raise ArgumentError' do
127
- expect { @p1.append!(@p2,7.0) }.to raise_error(ArgumentError)
128
- end
129
- end
130
-
131
- context 'offset equal to last value change offset' do
132
- it' should not raise ArgumentError' do
133
- expect { @p1.append!(@p2,7.5) }.not_to raise_error
134
- end
135
- end
136
-
137
- context 'offset greater than last value change offset' do
138
- it' should not raise ArgumentError' do
139
- expect { @p1.append!(@p2, 7.6) }.not_to raise_error
140
- end
141
-
142
- it 'should add on shifted value changes from second profile' do
143
- @p1.append!(@p2,10.0)
144
- @p1.value_changes[11.0].value.should eq 0.0
145
- @p1.value_changes[12.0].value.should eq 100.0
146
- end
147
- end
148
-
149
- context 'second profile start value equal to first profile last value' do
150
- it 'should not add value change at offset' do
151
- @p1.append!(@p2, 10.0)
152
- @p1.value_changes[10.0].should be nil
153
- end
154
- end
155
-
156
- context 'second profile start value not equal to first profile last value' do
157
- it 'should add value change at offset' do
158
- @p1.append!(@p3, 10.0)
159
- @p1.value_changes[10.0].should_not be nil
160
- end
161
- end
162
- end
163
- end