musicality 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/ChangeLog.md +13 -3
  3. data/README.md +8 -8
  4. data/bin/auditions +241 -0
  5. data/bin/collidify +4 -2
  6. data/bin/musicality +13 -2
  7. data/examples/composition/auto_counterpoint.rb +4 -4
  8. data/examples/composition/part_generator.rb +6 -6
  9. data/examples/composition/scale_exercise.rb +5 -5
  10. data/examples/notation/scores.rb +2 -2
  11. data/examples/notation/twinkle.rb +6 -6
  12. data/examples/notation/twinkle.score +3 -3
  13. data/lib/musicality.rb +6 -4
  14. data/lib/musicality/composition/dsl/part_methods.rb +12 -0
  15. data/lib/musicality/composition/dsl/score_dsl.rb +4 -4
  16. data/lib/musicality/composition/dsl/score_methods.rb +8 -2
  17. data/lib/musicality/notation/model/audition.rb +16 -0
  18. data/lib/musicality/notation/model/score.rb +31 -30
  19. data/lib/musicality/performance/supercollider/conductor.rb +2 -2
  20. data/lib/musicality/performance/supercollider/synthdefs.rb +30 -29
  21. data/lib/musicality/project/auditions_task.rb +28 -0
  22. data/lib/musicality/project/create_tasks.rb +15 -9
  23. data/lib/musicality/project/file_cleaner.rb +22 -5
  24. data/lib/musicality/project/file_raker.rb +29 -21
  25. data/lib/musicality/project/project.rb +218 -32
  26. data/lib/musicality/version.rb +1 -1
  27. data/musicality.gemspec +6 -4
  28. data/spec/composition/dsl/part_methods_spec.rb +24 -0
  29. data/spec/notation/conversion/score_conversion_spec.rb +2 -1
  30. data/spec/notation/conversion/score_converter_spec.rb +23 -23
  31. data/spec/notation/model/score_spec.rb +66 -46
  32. data/spec/performance/conversion/score_collator_spec.rb +29 -29
  33. data/spec/performance/midi/score_sequencing_spec.rb +5 -5
  34. metadata +10 -8
  35. data/lib/musicality/project/load_config.rb +0 -58
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c80e6fa5fd903b82f6a850f9780739197b0a3c56
4
- data.tar.gz: a110c580d68007bab242f7d7a647e4b583abd885
3
+ metadata.gz: f859d6a80c045507491575cffe0f999847d337bf
4
+ data.tar.gz: 1f342c1ea5e6402c817850d5589d857d4673541b
5
5
  SHA512:
6
- metadata.gz: bc1b333be0c54a2171344aba9458f5e32c2e4a8c2737431821525f058c6a827bd2eb351261cf2a6d159518c1e3281d79dadc621d6c48dbe58a8cec9adfa383f0
7
- data.tar.gz: a8e82d0a11291148498a6c54f17c0cb3db4f3071946946e0668cf7e5a335a6d8f3e782bf1137f49304e20efdd78b4441697fc980f6f229d73570122e4ecd6e54
6
+ metadata.gz: 741b7b48887b0015cc58816c5c689e9b3c214a7030ad83e83fbc7948349b1aed60bd606e09cf1de89d545496abfd520f4ccfd7664c767cb29f38c1110b1d6b45
7
+ data.tar.gz: 8bc2b08df2b6b1d574efe17185463f174c09aef58f3341455ba697a747b4d7ff4538d367cdcd6de0a9f8fb16646fb6ec7d2a3556a17aa01ccc6eb1d264b3083b
data/ChangeLog.md CHANGED
@@ -1,3 +1,14 @@
1
+ ### 0.11.0 / 2016-02-18
2
+ * Fix SuperCollider volume control to output stereo
3
+ * Add :keep_code keyword arg to SuperCollider::Conductor#perform
4
+ * Add convenience method Part#dynamic_change for adding a new dynamic change
5
+ * Add Project::update method and 'musicality update' subcommand
6
+ * Allow 'musicality new' to be ran on a non-empty directory. Existing scores directories will ignored, and existing project directories will be updated
7
+ * Add auditions executable. Add auditions rake tasks: auditions, auditions:wav, etc.
8
+ * Fix bug in Score#collated?
9
+ * Support interactive auditions using VLC on Linux
10
+ * Change Score::Tempo#initialize to only require the start tempo, making start meter an optional keyword argument
11
+
1
12
  ### 0.10.1 / 2016-01-08
2
13
  * Kill sclang process with Process.kill(9,pid) when running on Windows. Otherwise use Process.kill('INT',pid)
3
14
  * Delete .scd after it is processed
@@ -29,7 +40,7 @@
29
40
  * Add Part#settings. Create settings classes for LilyPond and MIDI
30
41
  * Add Part#lilypond_settings in lilypond_settings.rb and Part#midi_settings in midi_settings.rb
31
42
  * Add support for non-realtime SUperCollider performance
32
- * Make dynamic levels increase exponentially, like
43
+ * Make dynamic levels increase exponentially, like
33
44
  * In ScoreCollator#collate_changes, return new start value as well as dynamic changes, instead of always creating an initial immediate change
34
45
  * Add SynthDef class to be able to include SynthDefs in SuperCollider code
35
46
  * Add musicality project infrastructure
@@ -65,7 +76,7 @@
65
76
  * Conversion of both tempo-based scores (measured and unmeasured) directly to time-based score
66
77
  * Trimming of gradual changes, useful in collating scores
67
78
  * Refactoring of ValueComputer using Function and Transition utility classes
68
- * Add optional start_value for gradual changes, for making them absolute (by default they're relative)
79
+ * Add optional start_value for gradual changes, for making them absolute (by default they're relative)
69
80
  * Add 'with' kw arg to change pack/unpack methods, for converting start/end values
70
81
 
71
82
  ### 0.2.0 / 2014-11-24
@@ -90,4 +101,3 @@
90
101
  * Packing/unpacking to/from hash, using stringizing/parsing to condense, esp. for notes
91
102
  * Convert score to MIDI file via midilib gem
92
103
  * bin/midify command-line utility to run MIDI conversion on score YAML file
93
-
data/README.md CHANGED
@@ -74,25 +74,25 @@ include Meters
74
74
  include Dynamics
75
75
  include Pitches
76
76
 
77
- score = Score::Tempo.new(FOUR_FOUR, 120, title: "Twinkle, Twinkle, Little Star") do |s|
77
+ score = Score::Tempo.new(120, title: "Twinkle, Twinkle, Little Star") do |s|
78
78
  s.parts["rhand"] = Part.new(MF) do |p|
79
79
  a_notes = q(C4,C4,G4,G4,A4,A4) + h(G4) +
80
80
  q(F4,F4,E4,E4,D4,D4) + h(C4)
81
81
  b_notes = q(G4,G4,F4,F4,E4,E4) + h(D4)
82
82
  p.notes += a_notes + b_notes
83
83
  end
84
-
84
+
85
85
  s.parts["lhand"] = Part.new(MF) do |p|
86
86
  Cmaj = [C3,E3,G3]
87
87
  Fmaj = [F2,A2,C3]
88
88
  Gmaj = [G2,B2,D3]
89
-
90
- a_chords = h(Cmaj,Cmaj,Fmaj,Cmaj) +
89
+
90
+ a_chords = h(Cmaj,Cmaj,Fmaj,Cmaj) +
91
91
  h(Fmaj,Cmaj,Gmaj,Cmaj)
92
92
  b_chords = h(Cmaj,Fmaj,Cmaj,Gmaj)
93
93
  p.notes += a_chords + b_chords
94
94
  end
95
-
95
+
96
96
  s.program.push 0...4
97
97
  s.program.push 4...6
98
98
  s.program.push 4...6
@@ -131,7 +131,7 @@ The score DSL is an internal DSL (built on Ruby) that consists of a *score* bloc
131
131
 
132
132
  Here is an example of a score file.
133
133
  ```ruby
134
- tempo_score Meters::FOUR_FOUR, 120 do
134
+ tempo_score 120 do
135
135
  title "Twinkle, Twinkle, Little Star"
136
136
 
137
137
  Cmaj = [C3,E3,G3]
@@ -141,7 +141,7 @@ tempo_score Meters::FOUR_FOUR, 120 do
141
141
  notes(
142
142
  "rhand" => q(C4,C4,G4,G4,A4,A4) + h(G4) +
143
143
  q(F4,F4,E4,E4,D4,D4) + h(C4),
144
- "lhand" => h(Cmaj,Cmaj,Fmaj,Cmaj) +
144
+ "lhand" => h(Cmaj,Cmaj,Fmaj,Cmaj) +
145
145
  h(Fmaj,Cmaj,Gmaj,Cmaj)
146
146
  )
147
147
  end
@@ -166,7 +166,7 @@ include Pitches
166
166
 
167
167
  dsl = ScoreDSL.load 'twinkle.score'
168
168
  score = dsl.score
169
- ```
169
+ ```
170
170
 
171
171
 
172
172
  ## Musicality Projects
data/bin/auditions ADDED
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ exe_name = File.basename(__FILE__)
4
+
5
+ doc = <<DOCOPT
6
+ Creates audition audio files from the given score's auditions.
7
+
8
+ In the default (non-interactive) mode, the audition output files are simply
9
+ created and then left for the user to play and delete on their own.
10
+
11
+ In the interactive audition mode, audition files will be played under control of
12
+ the user. Each audition file will have a status associated with it, and will be
13
+ one of:
14
+ - undecided
15
+ - rejected
16
+ - accepted
17
+
18
+ Playback will continue as long as there are still undecided auditions.
19
+ Once an audition is marked accepted/rejected it will be removed from the
20
+ playlist. When the audition ends, the final status of each audition will be
21
+ displayed. The user will then have the option of deleting the rejected
22
+ audition files.
23
+
24
+ During the interactive audition mode, the user can enter one of the following
25
+ commands at any time:
26
+ - accept: Accept the current audition and start playing the next audition
27
+ under consideration.
28
+ - reject: Reject the current audition and start playing the next audition
29
+ under consideration.
30
+ - prev: Play the previous audition that is still under consideration.
31
+ - next: Skip to the next audition that is still under consideration.
32
+ - quit: Stop the audition.
33
+
34
+ Usage:
35
+ #{exe_name} SCORE_FILE [options]
36
+
37
+ Arguments:
38
+ SCORE_FILE Score YAML file (packed as a Hash or not)
39
+
40
+ Options:
41
+ -i --interactive Enable interactive mode
42
+ --outdir=OUTD Dir where files will be put (defaults to same dir as
43
+ audition file). Will be created if needed.
44
+ --format=FORMAT Audio file format to output. Valid formats are FLAC, WAV, and AIFF [default: FLAC]
45
+ -h --help Show this screen.
46
+ --version Show version.
47
+ DOCOPT
48
+
49
+ require 'docopt'
50
+ begin
51
+ args = Docopt::docopt(doc)
52
+ puts args
53
+ rescue Docopt::Exit => e
54
+ puts e.message
55
+ exit
56
+ end
57
+
58
+ require 'musicality'
59
+
60
+ if args["--version"]
61
+ puts "#{exe_name} from musicality v#{Musicality::VERSION}"
62
+ exit
63
+ end
64
+
65
+ SCORE_FILE = args["SCORE_FILE"]
66
+ unless File.exists? SCORE_FILE
67
+ puts "Score file #{SCORE_FILE} does not exist. Aborting."
68
+ exit
69
+ end
70
+
71
+ require 'fileutils'
72
+
73
+ if args["--outdir"]
74
+ OUTDIR = args["--outdir"]
75
+ unless Dir.exists? OUTDIR
76
+ puts "Output directory #{OUTDIR} does not exist. Creating."
77
+ FileUtils.mkdir_p OUTDIR
78
+ unless Dir.exists? OUTDIR
79
+ puts "Output directory #{OUTDIR} could not be created. Aborting."
80
+ exit
81
+ end
82
+ end
83
+ else
84
+ OUTDIR = File.dirname(SCORE_FILE)
85
+ end
86
+
87
+ INTERACTIVE = args["--interactive"]
88
+ AUDIO_FORMAT = args["--format"].downcase
89
+ TEMPO_SAMPLE_RATE = 200
90
+ ALLOWED_FNAME_SYMBOLS = /[A-Za-z0-9_]/
91
+
92
+ unless ["flac","wav","aiff"].include?(AUDIO_FORMAT)
93
+ puts "Unsupported audio format #{AUDIO_FORMAT}. Aborting."
94
+ exit
95
+ end
96
+
97
+ require 'yaml'
98
+ include Musicality
99
+
100
+ print "Reading file '#{SCORE_FILE}'..."
101
+ score = YAML.load(File.read(SCORE_FILE))
102
+
103
+ if score.is_a? Hash
104
+ score = score.unpack
105
+ end
106
+ puts "done"
107
+
108
+ require 'open3'
109
+
110
+ score.auditions.each do |audition|
111
+ puts "Beginning audition for part '#{audition.part_name}'"
112
+
113
+ unless score.parts.has_key? audition.part_name
114
+ puts "Score does not have part #{audition.part_name} for audition. Aborting."
115
+ exit
116
+ end
117
+
118
+ score.program = audition.program
119
+ unless score.is_a? Score::Timed
120
+ print "Converting to timed score..."
121
+ score = score.to_timed(TEMPO_SAMPLE_RATE)
122
+ puts "done"
123
+ end
124
+
125
+ unless score.valid?
126
+ puts "Score is not valid. See errors:"
127
+ puts score.errors.join("\n")
128
+ exit
129
+ end
130
+
131
+ base_fpath = "#{OUTDIR}/#{audition.part_name}_"
132
+
133
+ output_files = []
134
+ audition.performers.each do |name, sc_settings|
135
+ if /[\W]/ =~ name
136
+ puts "Replacing Non-word characters found in #{name} with underscores."
137
+ name = name.gsub(/[\W]/,"_")
138
+ end
139
+
140
+ score.parts[audition.part_name].settings = [ sc_settings ]
141
+ conductor = SuperCollider::Conductor.new(score)
142
+
143
+ base_fpath2 = base_fpath + name
144
+ conductor.perform(base_fpath2, selected_parts: [audition.part_name])
145
+
146
+ input_fpath = base_fpath2 + ".osc"
147
+ output_fpath = base_fpath2 + "." + AUDIO_FORMAT
148
+
149
+ print "Rendering #{output_fpath}..."
150
+ `scsynth -N #{input_fpath} _ #{output_fpath} 44100 #{AUDIO_FORMAT} int16`
151
+ puts "done"
152
+
153
+ File.delete(input_fpath)
154
+ output_files.push output_fpath
155
+ end
156
+
157
+ if INTERACTIVE
158
+ def kill_pid pid
159
+ if OS.windows?
160
+ Process.kill 9, pid
161
+ else
162
+ Process.kill "INT", pid
163
+ end
164
+ end
165
+
166
+ undecided = output_files
167
+ rejected = []
168
+ accepted = []
169
+
170
+ user_input = ""
171
+ i = 0
172
+
173
+ user_input_thread = Thread.new do
174
+ while true
175
+ user_input = STDIN.gets.chomp
176
+ #STDOUT.puts "got user input \"#{user_input}\""
177
+ end
178
+ end
179
+
180
+ # launch VLC with the 'remote control' console interface
181
+ Open3.popen2("vlc --intf rc #{undecided.join(" ")}") do |stdin, stdout, wait_thread|
182
+ pid = wait_thread.pid
183
+
184
+ vlc_running = true
185
+ while vlc_running
186
+ case user_input
187
+ when "next", "prev"
188
+ stdin.puts user_input
189
+ when "accept","reject"
190
+ begin
191
+ stdout.read_nonblock(2**16)
192
+ rescue IO::WaitReadable
193
+ end
194
+ stdin.puts "status"
195
+
196
+ result = stdout.readline
197
+ i = undecided.find_index {|fstr| result.include? fstr }
198
+ raise "Could not find file from status output #{result}" if i.nil?
199
+
200
+ if user_input == "accept"
201
+ accepted.push undecided.delete_at(i)
202
+ else
203
+ rejected.push undecided.delete_at(i)
204
+ end
205
+ stdin.puts "next"
206
+ when "quit"
207
+ vlc_running = false
208
+ stdin.puts "quit"
209
+ end
210
+ user_input = ""
211
+
212
+ if undecided.empty?
213
+ vlc_running = false
214
+ stdin.puts "quit"
215
+ end
216
+ end
217
+ end
218
+
219
+ user_input_thread.kill
220
+
221
+ if undecided.any?
222
+ puts "Undecided:"
223
+ undecided.each { |f| puts " " + f }
224
+ puts
225
+ end
226
+
227
+ if rejected.any?
228
+ puts "Rejected:"
229
+ rejected.each { |f| puts " " + f }
230
+ puts
231
+ end
232
+
233
+ if accepted.any?
234
+ puts "Accepted:"
235
+ accepted.each { |f| puts " " + f }
236
+ puts
237
+ end
238
+
239
+ # TODO ask the user if they would like to delete the rejected auditions
240
+ end
241
+ end
data/bin/collidify CHANGED
@@ -25,6 +25,7 @@ Options:
25
25
  --srate=SRATE Sampling rate for converting tempo-based score to time-based
26
26
  score, and for sampling dynamic change values [default: 200]
27
27
  --leadtime=LT Time before performance begins (must be > 0) [default: 0.1]
28
+ --keepcode Do not delete the intermediate SuperCollider code file (.scd)
28
29
  -h --help Show this screen.
29
30
  --version Show version.
30
31
  DOCOPT
@@ -63,7 +64,8 @@ end
63
64
 
64
65
  VERBOSE = args["--verbose"]
65
66
  PARTS = args["PART"]
66
- LEADTIME = args["--leadtime"].to_f
67
+ LEAD_TIME = args["--leadtime"].to_f
68
+ KEEP_CODE = args["--keepcode"]
67
69
 
68
70
  require 'yaml'
69
71
  include Musicality
@@ -86,7 +88,7 @@ File.open(SCORE_FILE) do |fin|
86
88
  if score.valid?
87
89
  conductor = SuperCollider::Conductor.new(score)
88
90
 
89
- perform_kwargs = { :verbose => VERBOSE, :lead_time => LEADTIME }
91
+ perform_kwargs = { :verbose => VERBOSE, :lead_time => LEAD_TIME, :keep_code => KEEP_CODE }
90
92
  if PARTS.any?
91
93
  perform_kwargs[:selected_parts] = PARTS
92
94
  end
data/bin/musicality CHANGED
@@ -3,15 +3,23 @@
3
3
  exe_name = File.basename(__FILE__)
4
4
 
5
5
  doc = <<DOCOPT
6
- The 'new' subcommand wll create a new musicality project.
6
+ The 'new' subcommand wll create a new musicality project in the given directory.
7
+ If the given directory does not exist then it will be created. If it exists
8
+ then any existing project files will be updated as under the update subcommand.
9
+
10
+ The 'update' subcommand will update/restore the project files (Gemfile,
11
+ Rakefile, and config.yml) while preserving any modifications. Unless a
12
+ directory is specified, the current working directory is assumed to be the
13
+ project directory.
7
14
 
8
15
  Usage:
9
16
  #{exe_name} new PROJECT_DIR [options]
17
+ #{exe_name} update [PROJECT_DIR] [options]
10
18
  #{exe_name} -h | --help
11
19
  #{exe_name} --version
12
20
 
13
21
  Arguments:
14
- PROJECT_DIR Path of an empty directory or directory to be created
22
+ PROJECT_DIR project root directory
15
23
 
16
24
  Options:
17
25
  -h --help Show this screen.
@@ -36,4 +44,7 @@ end
36
44
 
37
45
  if args["new"]
38
46
  Musicality::Project.new(args["PROJECT_DIR"])
47
+ elsif args["update"]
48
+ project_dir = args["PROJECT_DIR"] || Dir.pwd
49
+ Musicality::Project.update(project_dir)
39
50
  end
@@ -13,13 +13,13 @@ def random_melody rhythm, pitch_palette
13
13
  pitch_sampler = RandomSampler.new(pitches,Probabilities.random(pitches.size))
14
14
  make_notes(rhythm, pitch_sampler.sample(rhythm.size))
15
15
  end
16
-
16
+
17
17
  def random_counterpoint rhythm, rhythm_palette, sample_rate, pitch_palette
18
18
  cpg = CounterpointGenerator.new(rhythm,rhythm_palette)
19
19
  counterpoint = cpg.best_solutions(25,0.5,sample_rate).sample
20
20
  pitches = pitch_palette.sample(rand(2..pitch_palette.size))
21
21
  pitch_sampler = RandomSampler.new(pitches,Probabilities.random(pitches.size))
22
- make_notes(counterpoint, pitch_sampler.sample(counterpoint.size))
22
+ make_notes(counterpoint, pitch_sampler.sample(counterpoint.size))
23
23
  end
24
24
 
25
25
  2.times do
@@ -43,10 +43,10 @@ end
43
43
  end
44
44
  end
45
45
 
46
- score = Score::Tempo.new(Meters::FOUR_FOUR, 120,
46
+ score = Score::Tempo.new(120,
47
47
  parts: { "bass" => bass, "guitar" => guitar },
48
48
  program: [ 0...([bass.duration,guitar.duration].min) ]
49
49
  )
50
50
 
51
51
  seq = ScoreSequencer.new(score.to_timed(200)).make_midi_seq
52
- File.open("./auto_counterpoint.mid", 'wb'){ |fout| seq.write(fout) }
52
+ File.open("./auto_counterpoint.mid", 'wb'){ |fout| seq.write(fout) }
@@ -11,12 +11,12 @@ major = Heptatonic::Prima::MAJOR
11
11
  pitch_seqs = {
12
12
  D4 => minor, C4 => major, E4 => minor, F4 => minor
13
13
  }.map do |pitch,scale_class|
14
- scale_class.to_pitch_seq(pitch)
14
+ scale_class.to_pitch_seq(pitch)
15
15
  end
16
16
 
17
17
  rhythm_seq2 = RepeatingSequence.new([3/8.to_r]*4)
18
18
 
19
- score = Score::Tempo.new(SIX_EIGHT,90) do |s|
19
+ score = Score::Tempo.new(90, start_meter: SIX_EIGHT) do |s|
20
20
  s.parts["main"] = Part.new(Dynamics::MF) do |p|
21
21
  p.settings.push MidiSettings::LEAD_SAWTOOTH
22
22
 
@@ -27,7 +27,7 @@ score = Score::Tempo.new(SIX_EIGHT,90) do |s|
27
27
  pitches = pitch_seqs.map { |pseq| pseq.at(poffsets).to_a }.flatten
28
28
  p.notes += make_notes(rhythm,pitches)
29
29
  end
30
-
30
+
31
31
  s.parts["bass"] = Part.new(Dynamics::MP) do |p|
32
32
  p.settings.push MidiSettings::LEAD_SQUARE
33
33
 
@@ -38,7 +38,7 @@ score = Score::Tempo.new(SIX_EIGHT,90) do |s|
38
38
  pitches = pitch_seqs.map { |pseq| pseq.at(poffsets).to_a }.flatten
39
39
  p.notes += make_notes(rhythm,pitches)
40
40
  end
41
-
41
+
42
42
  s.parts["pluck"] = Part.new(Dynamics::MP) do |p|
43
43
  p.settings.push MidiSettings::ORCHESTRAL_HARP
44
44
 
@@ -49,9 +49,9 @@ score = Score::Tempo.new(SIX_EIGHT,90) do |s|
49
49
  pitches = pitch_seqs.map { |pseq| pseq.at(poffsets).to_a }.flatten
50
50
  p.notes += make_notes(rhythm,pitches)
51
51
  end
52
-
52
+
53
53
  s.program = [0...s.measures_long]*2
54
54
  end
55
55
 
56
56
  seq = ScoreSequencer.new(score.to_timed(200)).make_midi_seq
57
- File.open("./part_generator.mid", 'wb'){ |fout| seq.write(fout) }
57
+ File.open("./part_generator.mid", 'wb'){ |fout| seq.write(fout) }