musicality 0.10.1 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
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) }