musicality 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (141) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +47 -0
  8. data/Rakefile +65 -0
  9. data/bin/midify +78 -0
  10. data/examples/hip.rb +32 -0
  11. data/examples/missed_connection.rb +26 -0
  12. data/examples/song1.rb +33 -0
  13. data/examples/song2.rb +32 -0
  14. data/lib/musicality/errors.rb +9 -0
  15. data/lib/musicality/notation/conversion/change_conversion.rb +19 -0
  16. data/lib/musicality/notation/conversion/measure_note_map.rb +40 -0
  17. data/lib/musicality/notation/conversion/measured_score_conversion.rb +70 -0
  18. data/lib/musicality/notation/conversion/measured_score_converter.rb +95 -0
  19. data/lib/musicality/notation/conversion/note_time_converter.rb +68 -0
  20. data/lib/musicality/notation/conversion/tempo_conversion.rb +25 -0
  21. data/lib/musicality/notation/conversion/unmeasured_score_conversion.rb +47 -0
  22. data/lib/musicality/notation/conversion/unmeasured_score_converter.rb +64 -0
  23. data/lib/musicality/notation/model/articulations.rb +13 -0
  24. data/lib/musicality/notation/model/change.rb +62 -0
  25. data/lib/musicality/notation/model/dynamics.rb +12 -0
  26. data/lib/musicality/notation/model/link.rb +73 -0
  27. data/lib/musicality/notation/model/meter.rb +54 -0
  28. data/lib/musicality/notation/model/meters.rb +9 -0
  29. data/lib/musicality/notation/model/note.rb +120 -0
  30. data/lib/musicality/notation/model/part.rb +54 -0
  31. data/lib/musicality/notation/model/pitch.rb +163 -0
  32. data/lib/musicality/notation/model/pitches.rb +21 -0
  33. data/lib/musicality/notation/model/program.rb +53 -0
  34. data/lib/musicality/notation/model/score.rb +132 -0
  35. data/lib/musicality/notation/packing/change_packing.rb +46 -0
  36. data/lib/musicality/notation/packing/part_packing.rb +31 -0
  37. data/lib/musicality/notation/packing/program_packing.rb +16 -0
  38. data/lib/musicality/notation/packing/score_packing.rb +108 -0
  39. data/lib/musicality/notation/parsing/articulation_parsing.rb +264 -0
  40. data/lib/musicality/notation/parsing/articulation_parsing.treetop +59 -0
  41. data/lib/musicality/notation/parsing/convenience_methods.rb +74 -0
  42. data/lib/musicality/notation/parsing/duration_nodes.rb +21 -0
  43. data/lib/musicality/notation/parsing/duration_parsing.rb +205 -0
  44. data/lib/musicality/notation/parsing/duration_parsing.treetop +25 -0
  45. data/lib/musicality/notation/parsing/link_nodes.rb +35 -0
  46. data/lib/musicality/notation/parsing/link_parsing.rb +270 -0
  47. data/lib/musicality/notation/parsing/link_parsing.treetop +33 -0
  48. data/lib/musicality/notation/parsing/meter_parsing.rb +190 -0
  49. data/lib/musicality/notation/parsing/meter_parsing.treetop +29 -0
  50. data/lib/musicality/notation/parsing/note_node.rb +40 -0
  51. data/lib/musicality/notation/parsing/note_parsing.rb +229 -0
  52. data/lib/musicality/notation/parsing/note_parsing.treetop +28 -0
  53. data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.rb +289 -0
  54. data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.treetop +29 -0
  55. data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.rb +64 -0
  56. data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.treetop +17 -0
  57. data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.rb +86 -0
  58. data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.treetop +20 -0
  59. data/lib/musicality/notation/parsing/numbers/positive_float_parsing.rb +503 -0
  60. data/lib/musicality/notation/parsing/numbers/positive_float_parsing.treetop +33 -0
  61. data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.rb +95 -0
  62. data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.treetop +19 -0
  63. data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.rb +84 -0
  64. data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.treetop +19 -0
  65. data/lib/musicality/notation/parsing/parseable.rb +30 -0
  66. data/lib/musicality/notation/parsing/pitch_node.rb +23 -0
  67. data/lib/musicality/notation/parsing/pitch_parsing.rb +448 -0
  68. data/lib/musicality/notation/parsing/pitch_parsing.treetop +52 -0
  69. data/lib/musicality/notation/parsing/segment_parsing.rb +141 -0
  70. data/lib/musicality/notation/parsing/segment_parsing.treetop +23 -0
  71. data/lib/musicality/notation/util/interpolation.rb +16 -0
  72. data/lib/musicality/notation/util/piecewise_function.rb +122 -0
  73. data/lib/musicality/notation/util/value_computer.rb +170 -0
  74. data/lib/musicality/performance/conversion/glissando_converter.rb +34 -0
  75. data/lib/musicality/performance/conversion/note_sequence_extractor.rb +98 -0
  76. data/lib/musicality/performance/conversion/portamento_converter.rb +24 -0
  77. data/lib/musicality/performance/conversion/score_collator.rb +126 -0
  78. data/lib/musicality/performance/midi/midi_events.rb +34 -0
  79. data/lib/musicality/performance/midi/midi_util.rb +31 -0
  80. data/lib/musicality/performance/midi/part_sequencer.rb +123 -0
  81. data/lib/musicality/performance/midi/score_sequencer.rb +45 -0
  82. data/lib/musicality/performance/model/note_attacks.rb +19 -0
  83. data/lib/musicality/performance/model/note_sequence.rb +111 -0
  84. data/lib/musicality/performance/util/note_linker.rb +28 -0
  85. data/lib/musicality/performance/util/optimization.rb +31 -0
  86. data/lib/musicality/validatable.rb +38 -0
  87. data/lib/musicality/version.rb +3 -0
  88. data/lib/musicality.rb +81 -0
  89. data/musicality.gemspec +30 -0
  90. data/spec/musicality_spec.rb +7 -0
  91. data/spec/notation/conversion/change_conversion_spec.rb +40 -0
  92. data/spec/notation/conversion/measure_note_map_spec.rb +73 -0
  93. data/spec/notation/conversion/measured_score_conversion_spec.rb +141 -0
  94. data/spec/notation/conversion/measured_score_converter_spec.rb +329 -0
  95. data/spec/notation/conversion/note_time_converter_spec.rb +81 -0
  96. data/spec/notation/conversion/tempo_conversion_spec.rb +40 -0
  97. data/spec/notation/conversion/unmeasured_score_conversion_spec.rb +71 -0
  98. data/spec/notation/conversion/unmeasured_score_converter_spec.rb +116 -0
  99. data/spec/notation/model/change_spec.rb +90 -0
  100. data/spec/notation/model/link_spec.rb +83 -0
  101. data/spec/notation/model/meter_spec.rb +97 -0
  102. data/spec/notation/model/note_spec.rb +183 -0
  103. data/spec/notation/model/part_spec.rb +69 -0
  104. data/spec/notation/model/pitch_spec.rb +180 -0
  105. data/spec/notation/model/program_spec.rb +50 -0
  106. data/spec/notation/model/score_spec.rb +211 -0
  107. data/spec/notation/packing/change_packing_spec.rb +153 -0
  108. data/spec/notation/packing/part_packing_spec.rb +66 -0
  109. data/spec/notation/packing/program_packing_spec.rb +33 -0
  110. data/spec/notation/packing/score_packing_spec.rb +301 -0
  111. data/spec/notation/parsing/articulation_parsing_spec.rb +23 -0
  112. data/spec/notation/parsing/convenience_methods_spec.rb +99 -0
  113. data/spec/notation/parsing/duration_nodes_spec.rb +83 -0
  114. data/spec/notation/parsing/duration_parsing_spec.rb +70 -0
  115. data/spec/notation/parsing/link_nodes_spec.rb +30 -0
  116. data/spec/notation/parsing/link_parsing_spec.rb +13 -0
  117. data/spec/notation/parsing/meter_parsing_spec.rb +23 -0
  118. data/spec/notation/parsing/note_node_spec.rb +87 -0
  119. data/spec/notation/parsing/note_parsing_spec.rb +46 -0
  120. data/spec/notation/parsing/numbers/nonnegative_float_spec.rb +28 -0
  121. data/spec/notation/parsing/numbers/nonnegative_integer_spec.rb +11 -0
  122. data/spec/notation/parsing/numbers/nonnegative_rational_spec.rb +11 -0
  123. data/spec/notation/parsing/numbers/positive_float_spec.rb +28 -0
  124. data/spec/notation/parsing/numbers/positive_integer_spec.rb +28 -0
  125. data/spec/notation/parsing/numbers/positive_rational_spec.rb +28 -0
  126. data/spec/notation/parsing/pitch_node_spec.rb +38 -0
  127. data/spec/notation/parsing/pitch_parsing_spec.rb +14 -0
  128. data/spec/notation/parsing/segment_parsing_spec.rb +27 -0
  129. data/spec/notation/util/value_computer_spec.rb +146 -0
  130. data/spec/performance/conversion/glissando_converter_spec.rb +93 -0
  131. data/spec/performance/conversion/note_sequence_extractor_spec.rb +230 -0
  132. data/spec/performance/conversion/portamento_converter_spec.rb +91 -0
  133. data/spec/performance/conversion/score_collator_spec.rb +183 -0
  134. data/spec/performance/midi/midi_util_spec.rb +110 -0
  135. data/spec/performance/midi/part_sequencer_spec.rb +40 -0
  136. data/spec/performance/midi/score_sequencer_spec.rb +50 -0
  137. data/spec/performance/model/note_sequence_spec.rb +147 -0
  138. data/spec/performance/util/note_linker_spec.rb +68 -0
  139. data/spec/performance/util/optimization_spec.rb +73 -0
  140. data/spec/spec_helper.rb +43 -0
  141. metadata +323 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 25e0c57192fe59be6b7839c2eb1b7a0ebfca7d5e
4
+ data.tar.gz: 3e5b8efeddfdcb1a7fa4b1f78a8752204109779e
5
+ SHA512:
6
+ metadata.gz: 41c88ca97959bdb89f55e27a69505a77a3f045c5ef6406d4c9d3fba61969f60ef3a7e9ad3e94d4323217b3bc45d6d972cd06ee070448a6f34c69b3b93b41fd49
7
+ data.tar.gz: b6019f6508f05bfd4d8cfb82e6bd9b724b3a4b6af0e1dde1f2cf2eb5d625836fcc00a47162f4d2457a74d3cd9b81f14fb2f8ed8438eb96a4435595a9c155c10f
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /exec/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
11
+ *.bundle
12
+ *.so
13
+ *.o
14
+ *.a
15
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in musicality.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 James Tunnell
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # Musicality
2
+
3
+ The library is based around an abstract representation for music notation. From here, functions are built up to make composing elaborate pieces in this notation representation more manageable. Finally, music performance is supported by providing translation to common formats, like MIDI.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'musicality'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install musicality
20
+
21
+ ## Usage
22
+
23
+ Raw notation objects can be created like this:
24
+ ```ruby
25
+ require 'musicality'
26
+ include Musicality
27
+ include Pitches
28
+ include Dynamics
29
+
30
+ single = Note.quarter(Ab4)
31
+ rest = Note.quarter
32
+ chord = Note.whole([C3,E3,G3])
33
+ part = Part.new(MP, notes:[single,rest,chord])
34
+ ```
35
+
36
+ Or, a compact, string representation can be used, instead.
37
+ ```ruby
38
+ Part.new(FF, "/4Ab4 /4 1C3,E3,G3".to_notes)
39
+ ```
40
+
41
+ ## Contributing
42
+
43
+ 1. Fork it ( https://github.com/[my-github-username]/musicality/fork )
44
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
45
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
46
+ 4. Push to the branch (`git push origin my-new-feature`)
47
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,65 @@
1
+ # encoding: utf-8
2
+
3
+ require "bundler/gem_tasks"
4
+ require 'rake'
5
+ require 'rspec/core/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new
8
+
9
+ task :test => :spec
10
+ task :default => :spec
11
+
12
+ def rb_fname fname
13
+ basename = File.basename(fname, File.extname(fname))
14
+ dirname = File.dirname(fname)
15
+ "#{dirname}/#{basename}.rb"
16
+ end
17
+
18
+ task :build_parsers do
19
+ wd = Dir.pwd
20
+ Dir.chdir "lib/musicality/notation/parsing"
21
+ parser_files = Dir.glob(["**/*.treetop","**/*.tt"])
22
+
23
+ if parser_files.empty?
24
+ puts "No parsers found"
25
+ return
26
+ end
27
+
28
+ build_list = parser_files.select do |fname|
29
+ rb_name = rb_fname(fname)
30
+ !File.exists?(rb_name) || (File.mtime(fname) > File.mtime(rb_name))
31
+ end
32
+
33
+ if build_list.any?
34
+ puts "building parsers:"
35
+ build_list.each do |fname|
36
+ puts " #{fname} -> #{rb_fname(fname)}"
37
+ `tt -f #{fname}`
38
+ end
39
+ else
40
+ puts "Parsers are up-to-date"
41
+ end
42
+ Dir.chdir wd
43
+ end
44
+ task :spec => :build_parsers
45
+
46
+ task :make_examples do
47
+ current_dir = Dir.getwd
48
+ examples_dir = File.join(File.dirname(__FILE__), 'examples')
49
+ Dir.chdir examples_dir
50
+
51
+ examples = []
52
+ Dir.glob('**/*.rb') do |file|
53
+ examples.push File.expand_path(file)
54
+ end
55
+
56
+ examples.each do |example|
57
+ dirname = File.dirname(example)
58
+ filename = File.basename(example)
59
+
60
+ Dir.chdir dirname
61
+ ruby filename
62
+ end
63
+
64
+ Dir.chdir current_dir
65
+ end
data/bin/midify ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ exe_name = File.basename(__FILE__)
4
+
5
+ doc = <<DOCOPT
6
+ Loads a musicality score from YAML file, and converts it to a MIDI file.
7
+
8
+ Usage:
9
+ #{exe_name} <input> [PART PROGRAM] ... [options]
10
+ #{exe_name} <input> <output> [PART PROGRAM] ... [options]
11
+ #{exe_name} -h | --help
12
+ #{exe_name} --version
13
+
14
+ Arguments:
15
+ input A musicality score file (may be packed as a hash) in YAML format
16
+ output Midi filename
17
+ PART name of a part in the score
18
+ PROGRAM MIDI program (instrument) number for the given part
19
+
20
+ Options:
21
+ --srate=SRATE sampling rate for converting tempo-based score to time-based
22
+ score, and for sampling dynamic change values [default: 200]
23
+ -h --help Show this screen.
24
+ --version Show version.
25
+
26
+ DOCOPT
27
+
28
+ require 'docopt'
29
+ begin
30
+ args = Docopt::docopt(doc)
31
+ puts args
32
+ rescue Docopt::Exit => e
33
+ puts e.message
34
+ exit
35
+ end
36
+
37
+ require 'yaml'
38
+ require 'musicality'
39
+ include Musicality
40
+
41
+ fin_name = args["<input>"]
42
+ File.open(fin_name) do |fin|
43
+ print "Reading file '#{fin_name}'..."
44
+ score = YAML.load(fin.read)
45
+
46
+ if score.is_a? Hash
47
+ score = Score.unpack(score)
48
+ end
49
+ puts "done"
50
+
51
+ unless score.is_a? Score::Timed
52
+ print "Converting to timed score..."
53
+ score = score.to_timed(args["--srate"])
54
+ puts "done"
55
+ end
56
+
57
+ if score.valid?
58
+ part_names = args["PART"]
59
+ program_nums = args["PROGRAM"].map {|str| str.to_i }
60
+ instr_map = Hash[[part_names,program_nums].transpose]
61
+
62
+ print "Making MIDI sequence..."
63
+ sequencer = ScoreSequencer.new(score)
64
+ seq = sequencer.make_midi_seq(instr_map)
65
+ puts "done"
66
+
67
+ fout_name = args["<output>"]
68
+ if fout_name.nil?
69
+ fout_name = "#{File.dirname(fin_name)}/#{File.basename(fin_name,File.extname(fin_name))}.mid"
70
+ end
71
+ print "Writing MIDI file '#{fout_name}'..."
72
+ File.open(fout_name, 'wb'){ |fout| seq.write(fout) }
73
+ puts "done"
74
+ else
75
+ puts "Score is not valid. See errors:"
76
+ puts score.errors.join("\n")
77
+ end
78
+ end
data/examples/hip.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'musicality'
2
+ require 'yaml'
3
+
4
+ include Musicality
5
+ include Pitches
6
+ include Articulations
7
+ include Meters
8
+
9
+ score = Score::Measured.new(FOUR_FOUR,120) do |s|
10
+ s.program = Program.new([0...2, 0...2,2...4,0...2])
11
+ s.parts["lead"] = Part.new(Dynamics::MF) do |p|
12
+ riff = "/6Bb3 /4 /12Db4= /6Db4= /36Db4 /36Eb4 /36Db4 /6Ab3 /12Db4 \
13
+ /6Bb3 /4 /12Db4= /4Db4= /8=Db4 /8C4".to_notes
14
+ p.notes = riff + riff.map {|n| n.transpose(2) }
15
+ end
16
+
17
+ s.parts["bass"] = Part.new(Dynamics::MP) do |p|
18
+ riff = "/6Bb2 /4 /3Ab2 /6F2 /12Ab2 \
19
+ /6Bb2 /4 /3Ab2 /4Ab2".to_notes
20
+ p.notes = riff + riff.map {|n| n.transpose(2) }
21
+ end
22
+ end
23
+
24
+ name = File.basename(__FILE__,".rb")
25
+
26
+ File.open("#{name}.yml", "w") do |file|
27
+ file.write score.to_yaml
28
+ end
29
+
30
+ File.open("#{name}_packed.yml", "w") do |file|
31
+ file.write score.pack.to_yaml
32
+ end
@@ -0,0 +1,26 @@
1
+ require 'musicality'
2
+ require 'yaml'
3
+
4
+ include Musicality
5
+ include Pitches
6
+ include Articulations
7
+ include Meters
8
+
9
+ score = Score::Measured.new(FOUR_FOUR, 120) do |s|
10
+ s.program = Program.new([0...2,0...6])
11
+ s.parts["bass"] = Part.new(Dynamics::MF) do |p|
12
+ p.notes = "/4Eb2 /4 /4Bb2 /4 /4Eb2 /8 /8B2 /4Bb2 /4Ab2".to_notes
13
+ p.notes += "/4Bb2 /8 /8F3= /2F3 /4Bb2 /8 /8F3= /2F3".to_notes
14
+ p.notes += "/4B2 /8 /8Gb3= /2Gb3 /8 /8Gb3= /2Gb3".to_notes
15
+ end
16
+ end
17
+
18
+ name = File.basename(__FILE__,".rb")
19
+
20
+ File.open("#{name}.yml", "w") do |file|
21
+ file.write score.to_yaml
22
+ end
23
+
24
+ File.open("#{name}_packed.yml", "w") do |file|
25
+ file.write score.pack.to_yaml
26
+ end
data/examples/song1.rb ADDED
@@ -0,0 +1,33 @@
1
+ require 'musicality'
2
+ require 'yaml'
3
+
4
+ include Musicality
5
+ include Pitches
6
+ include Articulations
7
+ include Meters
8
+
9
+ score = Score::Measured.new(FOUR_FOUR, 120) do |s|
10
+ s.program = Program.new([ 0...4.0, 0...4.0 ])
11
+
12
+ s.parts[1] = Part.new(Dynamics::MF) do |p|
13
+ p.notes = "3/8C2 /4Eb2 5/16F2 /16Eb2 \
14
+ /8 /4C2 /4Eb2 3/8 \
15
+ 3/8C2 /4Eb2 5/16F2 /16Eb2 \
16
+ /8 /4C2 /4Eb2".to_notes
17
+ end
18
+
19
+ s.parts[2] = Part.new(Dynamics::MF) do |p|
20
+ p.notes = "/8 /8Bb3 /8Bb3 /8Bb3 /8Bb3 /4C4 /4A3 /8G3 /8F3 5/16=G3 /16=F3 /8E3 /8 \
21
+ /8 /8Bb3 /8Bb3 /8Bb3 /8Bb3 /4C4 /8A3 /8E4 /8=E4 /8=D4 /8C4".to_notes
22
+ end
23
+ end
24
+
25
+ name = File.basename(__FILE__,".rb")
26
+
27
+ File.open("#{name}.yml", "w") do |file|
28
+ file.write score.to_yaml
29
+ end
30
+
31
+ File.open("#{name}_packed.yml", "w") do |file|
32
+ file.write score.pack.to_yaml
33
+ end
data/examples/song2.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'musicality'
2
+ require 'yaml'
3
+
4
+ include Musicality
5
+ include Pitches
6
+ include Meters
7
+
8
+ score = Score::Measured.new(FOUR_FOUR, 120) do |s|
9
+ s.program = Program.new([0...4.0, 0...4.0 ])
10
+
11
+ s.parts[1] = Part.new(Dynamics::MF) do |p|
12
+ p.notes = "1C4 1Bb3 1Ab3 /2G3 /2Bb3".to_notes
13
+ end
14
+
15
+ s.parts[2] = Part.new(Dynamics::MF) do |p|
16
+ p.notes = "3/8E5 1D5 1C5 5/8C5 /2C5 /2D5".to_notes
17
+ end
18
+
19
+ s.parts[3] = Part.new(Dynamics::MF) do |p|
20
+ p.notes = "/8 /4G5 /2F5 /4 /4F5 /2Eb5 /4 /4Eb5 /2Eb5 /8 /2Eb5 /2F5".to_notes
21
+ end
22
+ end
23
+
24
+ name = File.basename(__FILE__,".rb")
25
+
26
+ File.open("#{name}.yml", "w") do |file|
27
+ file.write score.to_yaml
28
+ end
29
+
30
+ File.open("#{name}_packed.yml", "w") do |file|
31
+ file.write score.pack.to_yaml
32
+ end
@@ -0,0 +1,9 @@
1
+ module Musicality
2
+ class NonZeroError < StandardError; end
3
+ class NegativeError < StandardError; end
4
+ class NonPositiveError < StandardError; end
5
+ class NonIntegerError < StandardError; end
6
+ class NonRationalError < StandardError; end
7
+ class NonIncreasingError < StandardError; end
8
+ class NotValidError < StandardError; end
9
+ end
@@ -0,0 +1,19 @@
1
+ module Musicality
2
+
3
+ class Change
4
+ class Immediate < Change
5
+ def offsets base_offset
6
+ [ base_offset ]
7
+ end
8
+ end
9
+
10
+ class Gradual < Change
11
+ def offsets base_offset
12
+ initial = base_offset - @elapsed
13
+ final = initial + @total_duration
14
+ [ initial, base_offset, base_offset + @impending, final ]
15
+ end
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,40 @@
1
+ module Musicality
2
+
3
+ module Conversion
4
+ # Converte offsets from measure-based to note-based.
5
+ # @param [Array] measure_offsets Measure offsets to be converted
6
+ # @param [Hash] measure_durations Map measure durations to measure offsets where the duration takes effect.
7
+ # @raise [NonZeroError] if first measure duration is not mapped to offset 0
8
+ def self.measure_note_map measure_offsets, measure_durations
9
+ mnoff_map = {}
10
+ moffs = measure_offsets.uniq.sort
11
+ mdurs = measure_durations.sort
12
+ cur_noff = 0.to_r
13
+ j = 0 # next measure offset to be converted
14
+
15
+ if mdurs[0][0] != 0
16
+ raise NonZeroError, "measure offset of 1st measure duration must be 0, not #{mdurs[0][0]}"
17
+ end
18
+
19
+ (0...mdurs.size).each do |i|
20
+ cur_moff, cur_mdur = mdurs[i]
21
+ if i < (mdurs.size - 1)
22
+ next_moff = mdurs[i+1][0]
23
+ else
24
+ next_moff = Float::INFINITY
25
+ end
26
+
27
+ while(j < moffs.size && moffs[j] <= next_moff) do
28
+ moff = moffs[j]
29
+ mnoff_map[moff] = cur_noff + (moff - cur_moff)*cur_mdur
30
+ j += 1
31
+ end
32
+
33
+ cur_noff += (next_moff - cur_moff) * cur_mdur
34
+ end
35
+
36
+ return mnoff_map
37
+ end
38
+ end
39
+
40
+ end
@@ -0,0 +1,70 @@
1
+ module Musicality
2
+
3
+ class Score
4
+ class Measured < TempoBased
5
+ # Convert to unmeasured score by converting measure-based offsets to
6
+ # note-based offsets, and eliminating the use of meters. Also, tempo is
7
+ # coverted from beats-per-minute to quarter-notes per minute.
8
+ def to_unmeasured
9
+ MeasuredScoreConverter.new(self).convert_score
10
+ end
11
+
12
+ def to_timed tempo_sample_rate
13
+ unmeasured = MeasuredScoreConverter.new(self).convert_score
14
+ unmeasured.to_timed(tempo_sample_rate)
15
+ end
16
+
17
+ def measure_note_map
18
+ Conversion::measure_note_map(measure_offsets,measure_durations)
19
+ end
20
+
21
+ def measure_offsets
22
+ moffs = Set.new([0.to_r])
23
+
24
+ @tempo_changes.each do |moff,change|
25
+ moffs += change.offsets(moff)
26
+ end
27
+
28
+ @meter_changes.keys.each {|moff| moffs.add(moff) }
29
+
30
+ @parts.values.each do |part|
31
+ part.dynamic_changes.each do |moff,change|
32
+ moffs += change.offsets(moff)
33
+ end
34
+ end
35
+
36
+ @program.segments.each do |seg|
37
+ moffs.add(seg.first)
38
+ moffs.add(seg.last)
39
+ end
40
+
41
+ return moffs.sort
42
+ end
43
+
44
+ def beat_durations
45
+ bdurs = @meter_changes.map do |offset,change|
46
+ [ offset, change.value.beat_duration ]
47
+ end.sort
48
+
49
+ if bdurs.empty? || bdurs[0][0] != 0
50
+ bdurs.unshift([0,@start_meter.beat_duration])
51
+ end
52
+
53
+ return bdurs
54
+ end
55
+
56
+ def measure_durations
57
+ mdurs = @meter_changes.map do |offset,change|
58
+ [ offset, change.value.measure_duration ]
59
+ end.sort
60
+
61
+ if mdurs.empty? || mdurs[0][0] != 0
62
+ mdurs.unshift([0,@start_meter.measure_duration])
63
+ end
64
+
65
+ return Hash[ mdurs ]
66
+ end
67
+ end
68
+ end
69
+
70
+ end
@@ -0,0 +1,95 @@
1
+ module Musicality
2
+
3
+ # Converts measured score to unmeasured by converting measure-based offsets
4
+ # and durations to note-based offsets, and eliminating the use of meters.
5
+ # Also, tempo is coverted from beats-per-minute to quarter-notes per minute.
6
+ class MeasuredScoreConverter
7
+ def initialize score
8
+ unless score.valid?
9
+ raise NotValidError, "The given score can not be converted because \
10
+ it is invalid, with these errors: #{score.errors}"
11
+ end
12
+
13
+ @score = score
14
+ @mnoff_map = score.measure_note_map
15
+ end
16
+
17
+ def convert_score
18
+ Score::Unmeasured.new(convert_start_tempo,
19
+ parts: convert_parts, program: convert_program,
20
+ tempo_changes: convert_tempo_changes)
21
+ end
22
+
23
+ def convert_parts
24
+ Hash[ @score.parts.map do |name,part|
25
+ new_dcs = Hash[ part.dynamic_changes.map do |moff,change|
26
+ case change
27
+ when Change::Immediate
28
+ [@mnoff_map[moff],change.clone]
29
+ when Change::Gradual
30
+ noff1, noff2, noff3, noff4 = change.offsets(moff).map {|x| @mnoff_map[x] }
31
+ [noff2, Change::Gradual.new(change.value,
32
+ noff3-noff2, noff2-noff1, noff4-noff3)]
33
+ end
34
+ end ]
35
+ new_notes = part.notes.map {|n| n.clone }
36
+ [name, Part.new(part.start_dynamic,
37
+ notes: new_notes, dynamic_changes: new_dcs)]
38
+ end ]
39
+ end
40
+
41
+ def convert_program
42
+ Program.new(
43
+ @score.program.segments.map do |seg|
44
+ @mnoff_map[seg.first]...@mnoff_map[seg.last]
45
+ end
46
+ )
47
+ end
48
+
49
+ def convert_start_tempo
50
+ Tempo::BPM.to_qnpm(@score.start_tempo, @score.start_meter.beat_duration)
51
+ end
52
+
53
+ def convert_tempo_changes
54
+ tcs = {}
55
+ bdurs = @score.beat_durations
56
+
57
+ @score.tempo_changes.each do |moff,change|
58
+ bdur = bdurs.select {|x,y| x <= moff}.max[1]
59
+ tempo = change.value
60
+
61
+ case change
62
+ when Change::Immediate
63
+ tcs[@mnoff_map[moff]] = Change::Immediate.new(Tempo::BPM.to_qnpm(tempo,bdur))
64
+ when Change::Gradual
65
+ start_moff, end_moff = moff, moff + change.duration
66
+ start_noff, end_noff = @mnoff_map[start_moff], @mnoff_map[end_moff]
67
+
68
+ initial_moff, final_moff = start_moff - change.elapsed, end_moff + change.remaining
69
+ initial_noff, final_noff = @mnoff_map[initial_moff], @mnoff_map[final_moff]
70
+
71
+ more_bdurs = bdurs.select {|x,y| x > start_moff && x < end_moff }
72
+ cur_noff, cur_bdur = start_noff, bdur
73
+
74
+ more_bdurs.each do |next_moff, next_bdur|
75
+ next_noff = @mnoff_map[next_moff]
76
+ elapsed = cur_noff - initial_noff
77
+ impending = next_noff - cur_noff
78
+ remaining = final_noff - next_noff
79
+ tempo2 = Tempo::BPM.to_qnpm(tempo, cur_bdur)
80
+ tcs[cur_noff] = Change::Gradual.new(tempo2, impending, elapsed, remaining)
81
+ cur_noff, cur_bdur = next_noff, next_bdur
82
+ end
83
+ elapsed = cur_noff - initial_noff
84
+ impending = end_noff - cur_noff
85
+ remaining = final_noff - end_noff
86
+ tempo2 = Tempo::BPM.to_qnpm(tempo, cur_bdur)
87
+ tcs[cur_noff] = Change::Gradual.new(tempo2, impending, elapsed, remaining)
88
+ end
89
+ end
90
+
91
+ return tcs
92
+ end
93
+ end
94
+
95
+ end
@@ -0,0 +1,68 @@
1
+ module Musicality
2
+
3
+ # Convert offsets in unmeasured note time to just plain time.
4
+ class NoteTimeConverter
5
+ # @param [ValueComputer] tempo_computer Given an offset, returns tempo
6
+ # value in quarter-notes-per-minute
7
+ # @param [Numeric] sample_rate Rate at which tempo values are sampled
8
+ # in the conversion (samples/sec).
9
+ def initialize tempo_computer, sample_rate
10
+ @tempo_computer = tempo_computer
11
+ @sample_period = Rational(1,sample_rate)
12
+ end
13
+
14
+ def notes_per_second_at offset
15
+ Tempo::QNPM.to_nps(@tempo_computer.value_at offset)
16
+ end
17
+
18
+ # Calculate the time elapsed between given start/end note offset. Using the
19
+ # notes-per-second values over this interval, note duration for each sample is
20
+ # known and accumulated as samples are taken. When accumulated note duration
21
+ # passes the given desired duration (end_offset - start_offset), the number of
22
+ # samples taken will indicated the corresponding time duration. Then there
23
+ # is adjustment for last sample taken, which likely goes past the desired note
24
+ # duration.
25
+ #
26
+ # @param [Numeric] start_offset the starting note offset.
27
+ # @param [Numeric] end_offset the ending note offset.
28
+ # @raise [ArgumentError] if end offset is less than starting offset.
29
+ def time_elapsed start_offset, end_offset
30
+ raise ArgumentError "note end is less than note begin" if end_offset < start_offset
31
+
32
+ time_samples = 0
33
+ offset = start_offset
34
+
35
+ while offset < end_offset
36
+ notes_per_sec = notes_per_second_at offset
37
+ notes_per_sample = notes_per_sec * @sample_period
38
+
39
+ if (offset + notes_per_sample) > end_offset
40
+ #interpolate between offset and end_offset
41
+ perc = (end_offset - offset) / notes_per_sample
42
+ time_samples += perc
43
+ offset = end_offset
44
+ else
45
+ time_samples += 1
46
+ offset += notes_per_sample
47
+ end
48
+ end
49
+
50
+ return time_samples * @sample_period
51
+ end
52
+
53
+ #map absolute note offsets to relative time offsets
54
+ def note_time_map offsets
55
+ time_counter = 0.0
56
+ sorted_offsets = offsets.sort
57
+ note_time_map = { sorted_offsets.first => time_counter }
58
+
59
+ for i in 1...sorted_offsets.count do
60
+ time_counter += time_elapsed(sorted_offsets[i-1], sorted_offsets[i])
61
+ note_time_map[sorted_offsets[i]] = time_counter
62
+ end
63
+
64
+ return note_time_map
65
+ end
66
+ end
67
+
68
+ end