musicality 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +47 -0
- data/Rakefile +65 -0
- data/bin/midify +78 -0
- data/examples/hip.rb +32 -0
- data/examples/missed_connection.rb +26 -0
- data/examples/song1.rb +33 -0
- data/examples/song2.rb +32 -0
- data/lib/musicality/errors.rb +9 -0
- data/lib/musicality/notation/conversion/change_conversion.rb +19 -0
- data/lib/musicality/notation/conversion/measure_note_map.rb +40 -0
- data/lib/musicality/notation/conversion/measured_score_conversion.rb +70 -0
- data/lib/musicality/notation/conversion/measured_score_converter.rb +95 -0
- data/lib/musicality/notation/conversion/note_time_converter.rb +68 -0
- data/lib/musicality/notation/conversion/tempo_conversion.rb +25 -0
- data/lib/musicality/notation/conversion/unmeasured_score_conversion.rb +47 -0
- data/lib/musicality/notation/conversion/unmeasured_score_converter.rb +64 -0
- data/lib/musicality/notation/model/articulations.rb +13 -0
- data/lib/musicality/notation/model/change.rb +62 -0
- data/lib/musicality/notation/model/dynamics.rb +12 -0
- data/lib/musicality/notation/model/link.rb +73 -0
- data/lib/musicality/notation/model/meter.rb +54 -0
- data/lib/musicality/notation/model/meters.rb +9 -0
- data/lib/musicality/notation/model/note.rb +120 -0
- data/lib/musicality/notation/model/part.rb +54 -0
- data/lib/musicality/notation/model/pitch.rb +163 -0
- data/lib/musicality/notation/model/pitches.rb +21 -0
- data/lib/musicality/notation/model/program.rb +53 -0
- data/lib/musicality/notation/model/score.rb +132 -0
- data/lib/musicality/notation/packing/change_packing.rb +46 -0
- data/lib/musicality/notation/packing/part_packing.rb +31 -0
- data/lib/musicality/notation/packing/program_packing.rb +16 -0
- data/lib/musicality/notation/packing/score_packing.rb +108 -0
- data/lib/musicality/notation/parsing/articulation_parsing.rb +264 -0
- data/lib/musicality/notation/parsing/articulation_parsing.treetop +59 -0
- data/lib/musicality/notation/parsing/convenience_methods.rb +74 -0
- data/lib/musicality/notation/parsing/duration_nodes.rb +21 -0
- data/lib/musicality/notation/parsing/duration_parsing.rb +205 -0
- data/lib/musicality/notation/parsing/duration_parsing.treetop +25 -0
- data/lib/musicality/notation/parsing/link_nodes.rb +35 -0
- data/lib/musicality/notation/parsing/link_parsing.rb +270 -0
- data/lib/musicality/notation/parsing/link_parsing.treetop +33 -0
- data/lib/musicality/notation/parsing/meter_parsing.rb +190 -0
- data/lib/musicality/notation/parsing/meter_parsing.treetop +29 -0
- data/lib/musicality/notation/parsing/note_node.rb +40 -0
- data/lib/musicality/notation/parsing/note_parsing.rb +229 -0
- data/lib/musicality/notation/parsing/note_parsing.treetop +28 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.rb +289 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.treetop +29 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.rb +64 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.treetop +17 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.rb +86 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.treetop +20 -0
- data/lib/musicality/notation/parsing/numbers/positive_float_parsing.rb +503 -0
- data/lib/musicality/notation/parsing/numbers/positive_float_parsing.treetop +33 -0
- data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.rb +95 -0
- data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.treetop +19 -0
- data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.rb +84 -0
- data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.treetop +19 -0
- data/lib/musicality/notation/parsing/parseable.rb +30 -0
- data/lib/musicality/notation/parsing/pitch_node.rb +23 -0
- data/lib/musicality/notation/parsing/pitch_parsing.rb +448 -0
- data/lib/musicality/notation/parsing/pitch_parsing.treetop +52 -0
- data/lib/musicality/notation/parsing/segment_parsing.rb +141 -0
- data/lib/musicality/notation/parsing/segment_parsing.treetop +23 -0
- data/lib/musicality/notation/util/interpolation.rb +16 -0
- data/lib/musicality/notation/util/piecewise_function.rb +122 -0
- data/lib/musicality/notation/util/value_computer.rb +170 -0
- data/lib/musicality/performance/conversion/glissando_converter.rb +34 -0
- data/lib/musicality/performance/conversion/note_sequence_extractor.rb +98 -0
- data/lib/musicality/performance/conversion/portamento_converter.rb +24 -0
- data/lib/musicality/performance/conversion/score_collator.rb +126 -0
- data/lib/musicality/performance/midi/midi_events.rb +34 -0
- data/lib/musicality/performance/midi/midi_util.rb +31 -0
- data/lib/musicality/performance/midi/part_sequencer.rb +123 -0
- data/lib/musicality/performance/midi/score_sequencer.rb +45 -0
- data/lib/musicality/performance/model/note_attacks.rb +19 -0
- data/lib/musicality/performance/model/note_sequence.rb +111 -0
- data/lib/musicality/performance/util/note_linker.rb +28 -0
- data/lib/musicality/performance/util/optimization.rb +31 -0
- data/lib/musicality/validatable.rb +38 -0
- data/lib/musicality/version.rb +3 -0
- data/lib/musicality.rb +81 -0
- data/musicality.gemspec +30 -0
- data/spec/musicality_spec.rb +7 -0
- data/spec/notation/conversion/change_conversion_spec.rb +40 -0
- data/spec/notation/conversion/measure_note_map_spec.rb +73 -0
- data/spec/notation/conversion/measured_score_conversion_spec.rb +141 -0
- data/spec/notation/conversion/measured_score_converter_spec.rb +329 -0
- data/spec/notation/conversion/note_time_converter_spec.rb +81 -0
- data/spec/notation/conversion/tempo_conversion_spec.rb +40 -0
- data/spec/notation/conversion/unmeasured_score_conversion_spec.rb +71 -0
- data/spec/notation/conversion/unmeasured_score_converter_spec.rb +116 -0
- data/spec/notation/model/change_spec.rb +90 -0
- data/spec/notation/model/link_spec.rb +83 -0
- data/spec/notation/model/meter_spec.rb +97 -0
- data/spec/notation/model/note_spec.rb +183 -0
- data/spec/notation/model/part_spec.rb +69 -0
- data/spec/notation/model/pitch_spec.rb +180 -0
- data/spec/notation/model/program_spec.rb +50 -0
- data/spec/notation/model/score_spec.rb +211 -0
- data/spec/notation/packing/change_packing_spec.rb +153 -0
- data/spec/notation/packing/part_packing_spec.rb +66 -0
- data/spec/notation/packing/program_packing_spec.rb +33 -0
- data/spec/notation/packing/score_packing_spec.rb +301 -0
- data/spec/notation/parsing/articulation_parsing_spec.rb +23 -0
- data/spec/notation/parsing/convenience_methods_spec.rb +99 -0
- data/spec/notation/parsing/duration_nodes_spec.rb +83 -0
- data/spec/notation/parsing/duration_parsing_spec.rb +70 -0
- data/spec/notation/parsing/link_nodes_spec.rb +30 -0
- data/spec/notation/parsing/link_parsing_spec.rb +13 -0
- data/spec/notation/parsing/meter_parsing_spec.rb +23 -0
- data/spec/notation/parsing/note_node_spec.rb +87 -0
- data/spec/notation/parsing/note_parsing_spec.rb +46 -0
- data/spec/notation/parsing/numbers/nonnegative_float_spec.rb +28 -0
- data/spec/notation/parsing/numbers/nonnegative_integer_spec.rb +11 -0
- data/spec/notation/parsing/numbers/nonnegative_rational_spec.rb +11 -0
- data/spec/notation/parsing/numbers/positive_float_spec.rb +28 -0
- data/spec/notation/parsing/numbers/positive_integer_spec.rb +28 -0
- data/spec/notation/parsing/numbers/positive_rational_spec.rb +28 -0
- data/spec/notation/parsing/pitch_node_spec.rb +38 -0
- data/spec/notation/parsing/pitch_parsing_spec.rb +14 -0
- data/spec/notation/parsing/segment_parsing_spec.rb +27 -0
- data/spec/notation/util/value_computer_spec.rb +146 -0
- data/spec/performance/conversion/glissando_converter_spec.rb +93 -0
- data/spec/performance/conversion/note_sequence_extractor_spec.rb +230 -0
- data/spec/performance/conversion/portamento_converter_spec.rb +91 -0
- data/spec/performance/conversion/score_collator_spec.rb +183 -0
- data/spec/performance/midi/midi_util_spec.rb +110 -0
- data/spec/performance/midi/part_sequencer_spec.rb +40 -0
- data/spec/performance/midi/score_sequencer_spec.rb +50 -0
- data/spec/performance/model/note_sequence_spec.rb +147 -0
- data/spec/performance/util/note_linker_spec.rb +68 -0
- data/spec/performance/util/optimization_spec.rb +73 -0
- data/spec/spec_helper.rb +43 -0
- 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
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.0
|
data/Gemfile
ADDED
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
|