musikov 0.15
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +26 -0
- data/README.md +46 -0
- data/bin/musikov +88 -0
- data/lib/musikov.rb +10 -0
- data/lib/musikov/markov_builder.rb +232 -0
- data/lib/musikov/markov_model.rb +99 -0
- data/lib/musikov/midi_parser.rb +73 -0
- data/lib/musikov/midi_writer.rb +67 -0
- data/spec/markov_builder_spec.rb +27 -0
- data/spec/markov_model_spec.rb +34 -0
- metadata +89 -0
data/LICENSE
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
Copyright (c) 2012, André de Amorim Fonseca
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
8
|
+
list of conditions and the following disclaimer.
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
11
|
+
and/or other materials provided with the distribution.
|
12
|
+
|
13
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
14
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
15
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
16
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
17
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
18
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
19
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
20
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
21
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
22
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
23
|
+
|
24
|
+
The views and conclusions contained in the software and documentation are those
|
25
|
+
of the authors and should not be interpreted as representing official policies,
|
26
|
+
either expressed or implied, of the FreeBSD Project.
|
data/README.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
## musikov
|
2
|
+
|
3
|
+
Random midi generator based on Markov Chains. (Make heavy use of [Midilib](https://github.com/jimm/midilib))
|
4
|
+
|
5
|
+
The model is quite simple: from a set of songs, a |Note, Duration| graph will be generated where the edges will indicate the probability of transitions between two of these states. A graph will be generated for each instrument on the input set of midifiles.
|
6
|
+
|
7
|
+
A song is generated randomically from an inital state, picking the subsequent states according the indicated probability.
|
8
|
+
|
9
|
+
More inform about Markov Chains : http://en.wikipedia.org/wiki/Markov_chain
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
### RubyGems instalation
|
14
|
+
|
15
|
+
```bash
|
16
|
+
gem install musikov
|
17
|
+
```
|
18
|
+
|
19
|
+
or, in order to update the gem:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
gem update musikov
|
23
|
+
```
|
24
|
+
|
25
|
+
You may need root privileges to install the gem.
|
26
|
+
|
27
|
+
## How to use it
|
28
|
+
|
29
|
+
First launch the musikov passing a midi file, or a folder containg midi files, as the main argument:
|
30
|
+
|
31
|
+
$ musikov generate -r path-to-midis [-o output_file]
|
32
|
+
|
33
|
+
The musikov will output a random midi file named output.mid (by default), or the indicated file if the option '-o' is used.
|
34
|
+
|
35
|
+
## TODO
|
36
|
+
|
37
|
+
- Define an option for the duration of the songs by time or by number of notes.
|
38
|
+
- Maybe classify different sacles in order to aproximate a generated song to the predominant scale.
|
39
|
+
|
40
|
+
## Author
|
41
|
+
|
42
|
+
Andre Fonseca <andre.amorimfonseca@gmail.com>
|
43
|
+
|
44
|
+
## License
|
45
|
+
|
46
|
+
Simplified BSD
|
data/bin/musikov
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#----------------------------------------------------------------------------------------
|
3
|
+
# The program takes an initial midi file (or folder with many midis) path
|
4
|
+
# from the command line. It will generate a random midi file from the input
|
5
|
+
# based on a Markov Chain model.
|
6
|
+
#
|
7
|
+
# Author:: André Fonseca
|
8
|
+
# License:: Simplified BSD
|
9
|
+
#----------------------------------------------------------------------------------------
|
10
|
+
|
11
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib') unless $LOAD_PATH.include?(File.dirname(__FILE__) + '/../lib')
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'musikov'
|
15
|
+
require 'thor'
|
16
|
+
require 'midilib/consts'
|
17
|
+
|
18
|
+
class MusikovStarter < Thor
|
19
|
+
|
20
|
+
include Thor::Actions
|
21
|
+
|
22
|
+
desc "generate", "generate random midi from input midi files"
|
23
|
+
method_option :resources, :aliases => "-r", :type => :array, :desc => "File or folder list containing the input midi files."
|
24
|
+
method_option :output_folder, :aliases => "-o", :type => :string, :desc => "Output folder."
|
25
|
+
def generate
|
26
|
+
begin
|
27
|
+
repository = Musikov::MarkovRepository.new
|
28
|
+
say "Learning midi sequence from resources :"
|
29
|
+
print_in_columns options[:resources]
|
30
|
+
repository.import options[:resources]
|
31
|
+
say "\nFiles successfully imported!", Thor::Shell::Color::GREEN
|
32
|
+
say "\nGenerating your random midi..."
|
33
|
+
generate_midi(repository, options[:output_folder])
|
34
|
+
rescue
|
35
|
+
say "There was a problem importing the selected files : #{$!.message} => #{$!.backtrace.join("\n")}", Thor::Shell::Color::RED
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
desc "generate_midi", "midi_generation task", :hide => true
|
40
|
+
def generate_midi(repository, path)
|
41
|
+
say "Select the instruments you would like to include in the output file:"
|
42
|
+
|
43
|
+
# Shows the instruments options to the user
|
44
|
+
opts = {}
|
45
|
+
repository.models_by_instrument.each_with_index { |(program_change, hash), index|
|
46
|
+
instrument = MIDI::GM_PATCH_NAMES[program_change]
|
47
|
+
say "#{index + 1} : #{instrument}"
|
48
|
+
opts[index + 1] = program_change
|
49
|
+
}
|
50
|
+
say "* : all instruments => Default"
|
51
|
+
|
52
|
+
# Asks for the instrument selection. Non selected instruments will be excluded from the model.
|
53
|
+
# By default it will include everything...
|
54
|
+
res = ask "\nSelect (ex: 1 2 3):", Thor::Shell::Color::YELLOW
|
55
|
+
instruments = []
|
56
|
+
res.split.each { |opt|
|
57
|
+
if (opt.to_i != 0) then
|
58
|
+
instruments << opts[opt.to_i] unless opts[opt.to_i].nil?
|
59
|
+
end
|
60
|
+
}
|
61
|
+
|
62
|
+
# Selects all instruments if none was pick
|
63
|
+
instruments += opts.values if instruments.empty?
|
64
|
+
|
65
|
+
# Excluding non selected instruments...
|
66
|
+
repository.models_by_instrument.delete_if { |instrument, markov_chain|
|
67
|
+
!instruments.include?(instrument) || markov_chain.frequencies.empty?
|
68
|
+
}
|
69
|
+
|
70
|
+
# Picking song from model...
|
71
|
+
sequences = {}
|
72
|
+
repository.models_by_instrument.each { |instrument, markov_chain|
|
73
|
+
sequences[instrument] = markov_chain.generate(nil, 50)
|
74
|
+
}
|
75
|
+
|
76
|
+
# Writing in the output file...
|
77
|
+
unless path.nil? then
|
78
|
+
repository.export(sequences, path)
|
79
|
+
else
|
80
|
+
repository.export(sequences)
|
81
|
+
end
|
82
|
+
|
83
|
+
say "\nSuccess: output file generated in output.mid!", Thor::Shell::Color::GREEN
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
MusikovStarter.start
|
data/lib/musikov.rb
ADDED
@@ -0,0 +1,232 @@
|
|
1
|
+
require 'musikov/midi_parser.rb'
|
2
|
+
require 'musikov/midi_writer.rb'
|
3
|
+
require 'musikov/markov_model.rb'
|
4
|
+
|
5
|
+
module Musikov
|
6
|
+
|
7
|
+
|
8
|
+
# This class holds the markov chains of each
|
9
|
+
# instrument built from the selected midi files.
|
10
|
+
class MarkovRepository
|
11
|
+
|
12
|
+
attr_accessor :models_by_instrument
|
13
|
+
|
14
|
+
# Initialize the map of markov chain by instrument
|
15
|
+
def initialize
|
16
|
+
@models_by_instrument = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
# Call parser over indicated paths and trigger the
|
20
|
+
# markov chain building process.
|
21
|
+
# * the paths can be either folders or files
|
22
|
+
# * the file indicated by the path must exist!
|
23
|
+
def import(paths = [])
|
24
|
+
parser = MidiParser.new(paths)
|
25
|
+
sequencies = parser.parse
|
26
|
+
|
27
|
+
builder = MarkovBuilder.new()
|
28
|
+
sequencies.each { |sequency|
|
29
|
+
builder.add(sequency)
|
30
|
+
}
|
31
|
+
|
32
|
+
@models_by_instrument = builder.build
|
33
|
+
end
|
34
|
+
|
35
|
+
# Export the generated chains
|
36
|
+
def export(sequence_hash = {}, path = ".")
|
37
|
+
writer = MidiWriter.new(path)
|
38
|
+
writer.write(sequence_hash)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
# This class is responsible for building the markov
|
44
|
+
# chain map from midi sequences. For every sequence,
|
45
|
+
# a "quarter" duration will be extracted and every note
|
46
|
+
# duration will be mapped in the possible values on the
|
47
|
+
# duration table. From a note and a specifc duration,
|
48
|
+
# MidiElements will be created representing a state on the
|
49
|
+
# markov chain.
|
50
|
+
class MarkovBuilder
|
51
|
+
|
52
|
+
####################
|
53
|
+
public
|
54
|
+
####################
|
55
|
+
|
56
|
+
# Initialize the hashes used to build the markov chain of note elements.
|
57
|
+
# * The value_chain is a hash containing a list of note events by instrument.
|
58
|
+
# * The duration table is a dynamic generated hash (generated from the "quarter" duration value) that maps
|
59
|
+
# a the duration of the notes to a string representation.
|
60
|
+
def initialize
|
61
|
+
@value_chain = Hash.new
|
62
|
+
@duration_table = Hash.new("unknow")
|
63
|
+
end
|
64
|
+
|
65
|
+
# Add a midi sequence to the model. From each sequence,
|
66
|
+
# different tracks (instruments) will be analyzed. Each
|
67
|
+
# sequence has a "tempo" that defines the "quarter" duration in ticks.
|
68
|
+
# From the "quarter" duration, creates a duration table containg (almost)
|
69
|
+
# every possible note duration.
|
70
|
+
# Creates markov chain elements by the value of the played note and the
|
71
|
+
# duration string mapped from the duration table.
|
72
|
+
def add(sequence)
|
73
|
+
# Obtains a quarter duration for this sequence
|
74
|
+
quarter_note_length = sequence.note_to_delta('quarter')
|
75
|
+
|
76
|
+
# Create the duration table based on the sequence's "quarter" value
|
77
|
+
create_duration_table(quarter_note_length)
|
78
|
+
|
79
|
+
# For each instrument on the sequence...
|
80
|
+
sequence.each { |track|
|
81
|
+
# Program change of the current track
|
82
|
+
program_change = nil
|
83
|
+
|
84
|
+
# Create a list of midi elements for an instrument
|
85
|
+
elements = []
|
86
|
+
|
87
|
+
# Iterates the track event list...
|
88
|
+
track.each { |event|
|
89
|
+
|
90
|
+
# Consider only "NoteOn" events since they represent the start of a note event (avoid duplication with "NoteOff").
|
91
|
+
if event.kind_of?(MIDI::NoteOnEvent) then
|
92
|
+
# From its correspondent "NoteOff" element, extract the duration of the note event.
|
93
|
+
duration = event.off.time_from_start - event.time_from_start + 1
|
94
|
+
|
95
|
+
# Maps the duration in ticks to a correspondent string on the duration table.
|
96
|
+
duration_representation = @duration_table[duration]
|
97
|
+
|
98
|
+
# In the case that the note do not correspond to anything in the table,
|
99
|
+
# we just truncate it to the closest value.
|
100
|
+
if duration_representation.nil? or duration_representation == "unknow" then
|
101
|
+
new_duration = 0
|
102
|
+
# Infinity value simulation
|
103
|
+
smallest_difference = 1.0/0
|
104
|
+
@duration_table.each { |key, value|
|
105
|
+
difference = (duration - key).abs
|
106
|
+
if difference < smallest_difference then
|
107
|
+
smallest_difference = difference
|
108
|
+
new_duration = key
|
109
|
+
end
|
110
|
+
}
|
111
|
+
duration_representation = @duration_table[new_duration]
|
112
|
+
end
|
113
|
+
|
114
|
+
# Create new markov chain state and put into the "elements" list
|
115
|
+
elements << MidiElement.new(event.note_to_s, duration_representation)
|
116
|
+
elsif event.kind_of?(MIDI::ProgramChange) then
|
117
|
+
program_change = event.program
|
118
|
+
end
|
119
|
+
}
|
120
|
+
|
121
|
+
if program_change.nil?
|
122
|
+
program_change = 1
|
123
|
+
end
|
124
|
+
|
125
|
+
@value_chain[program_change] ||= elements unless elements.empty?
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
# Build the markov chain for each instrument on the input midi file(s)
|
130
|
+
def build
|
131
|
+
model_by_instrument = {}
|
132
|
+
@value_chain.each{ |key, value|
|
133
|
+
model_by_instrument[key] = MarkovModel.new(value)
|
134
|
+
}
|
135
|
+
|
136
|
+
return model_by_instrument
|
137
|
+
end
|
138
|
+
|
139
|
+
####################
|
140
|
+
private
|
141
|
+
####################
|
142
|
+
|
143
|
+
# Creates the duration table from the "quarter" note duration in ticks.
|
144
|
+
# * The quarter note duration will change for every track!
|
145
|
+
def create_duration_table(quarter_note_length)
|
146
|
+
# Fuck it if the song have two dots or a combination of dots and "three"...
|
147
|
+
whole = (quarter_note_length * 4)
|
148
|
+
dotted_whole = (quarter_note_length * 6)
|
149
|
+
whole_triplet = (8 * quarter_note_length / 3)
|
150
|
+
half = (quarter_note_length * 2)
|
151
|
+
dotted_half = (quarter_note_length * 3)
|
152
|
+
half_three = (whole / 3)
|
153
|
+
dotted_quarter = (quarter_note_length * 1.5)
|
154
|
+
quarter_triplet = (half / 3)
|
155
|
+
eight = (quarter_note_length / 2)
|
156
|
+
eight_triplet = (quarter_note_length / 3)
|
157
|
+
dotted_eight = (3 * quarter_note_length / 4)
|
158
|
+
sixteenth = (quarter_note_length / 4)
|
159
|
+
sixteenth_triplet = (eight / 3)
|
160
|
+
dotted_sixteenth = (3 * quarter_note_length / 8)
|
161
|
+
thirty_second = (quarter_note_length / 8)
|
162
|
+
thirty_second_triplet = (sixteenth / 3)
|
163
|
+
dotted_thirty_second = (3 * quarter_note_length / 16)
|
164
|
+
sixty_fourth = (quarter_note_length / 16)
|
165
|
+
sixty_fourth_triplet = (thirty_second / 3)
|
166
|
+
dotted_sixty_fourth = (3 * quarter_note_length / 32)
|
167
|
+
|
168
|
+
@duration_table = {
|
169
|
+
whole => 'whole',
|
170
|
+
dotted_whole => 'dotted whole',
|
171
|
+
whole_triplet => 'whole triplet',
|
172
|
+
half => 'half',
|
173
|
+
dotted_half => 'dotted half',
|
174
|
+
half_three => 'half triplet',
|
175
|
+
quarter_note_length => 'quarter',
|
176
|
+
dotted_quarter => 'dotted quarter',
|
177
|
+
quarter_triplet => 'quarter triplet',
|
178
|
+
eight => 'eighth',
|
179
|
+
dotted_eight => 'dotted eighth',
|
180
|
+
eight_triplet => 'eighth triplet',
|
181
|
+
sixteenth => 'sixteenth',
|
182
|
+
dotted_sixteenth => 'dotted sixteenth',
|
183
|
+
sixteenth_triplet => 'sixteenth triplet',
|
184
|
+
thirty_second => 'thirtysecond',
|
185
|
+
dotted_thirty_second => 'dotted thirtysecond',
|
186
|
+
thirty_second_triplet => 'thirtysecond triplet',
|
187
|
+
sixty_fourth => 'sixtyfourth',
|
188
|
+
dotted_sixty_fourth => 'dotted sixtyfourth',
|
189
|
+
sixty_fourth_triplet => 'sixtyfourth triplet'
|
190
|
+
}
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
|
195
|
+
# This class represents a state on the markov chain. This state
|
196
|
+
# is built from a note and a duration string representation.
|
197
|
+
class MidiElement
|
198
|
+
|
199
|
+
attr_accessor :note, :duration
|
200
|
+
|
201
|
+
# Initializes the midi element by a note string and a string representation of the duration.
|
202
|
+
def initialize(note, duration)
|
203
|
+
@note = note
|
204
|
+
@duration = duration
|
205
|
+
end
|
206
|
+
|
207
|
+
# Overriding comparison to be used on "=="
|
208
|
+
def ==(comparison_object)
|
209
|
+
comparison_object.equal?(self) ||
|
210
|
+
(comparison_object.instance_of?(self.class) &&
|
211
|
+
@note == comparison_object.note &&
|
212
|
+
@duration == comparison_object.duration)
|
213
|
+
end
|
214
|
+
|
215
|
+
# Overriding comparison to be used on hashing
|
216
|
+
def eql?(comparison_object)
|
217
|
+
self.class.equal?(comparison_object.class) &&
|
218
|
+
@note == comparison_object.note &&
|
219
|
+
@duration == comparison_object.duration
|
220
|
+
end
|
221
|
+
|
222
|
+
def hash
|
223
|
+
@note.hash ^ @duration.hash
|
224
|
+
end
|
225
|
+
|
226
|
+
def to_s
|
227
|
+
"[Note: #{@note} Duration: #{@duration}]"
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
|
2
|
+
module Musikov
|
3
|
+
|
4
|
+
# This class represents a generic markov chain. It holds
|
5
|
+
# a hash of frequencies of subsequent states, for each state.
|
6
|
+
class MarkovModel
|
7
|
+
|
8
|
+
attr_reader :frequencies
|
9
|
+
|
10
|
+
###################
|
11
|
+
public
|
12
|
+
###################
|
13
|
+
|
14
|
+
# Initialize the hashes used to build the markov chain.
|
15
|
+
# Passes the initial array of values to be included in the model.
|
16
|
+
# * The "transitions" hash maps a state into another hash mapping the subsequent states to its number of subsequent occurrences
|
17
|
+
# * The "frequencies" hash maps a state into another hash mapping the subsequent states to a frequency indicating the probability of subsequent occurrences.
|
18
|
+
def initialize(value_chain = [])
|
19
|
+
@transitions = {}
|
20
|
+
@frequencies = {}
|
21
|
+
add_input(value_chain)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Generate a random sequence from a markov chain
|
25
|
+
# * The initial_value is a state where the random chain must start
|
26
|
+
# * The initial_value may be nil
|
27
|
+
# * The value_number indicates the number of elements on the result random sequence
|
28
|
+
def generate(initial_value, value_number)
|
29
|
+
generated_sequence = []
|
30
|
+
selected_value = initial_value
|
31
|
+
|
32
|
+
until generated_sequence.count == value_number do
|
33
|
+
selected_value = pick_value(rand(0.1..1.0), selected_value)
|
34
|
+
generated_sequence << selected_value
|
35
|
+
end
|
36
|
+
|
37
|
+
return generated_sequence
|
38
|
+
end
|
39
|
+
|
40
|
+
# Passes the argument array of state values to be included in the model.
|
41
|
+
def add_input(value_chain = [])
|
42
|
+
prev_value = nil
|
43
|
+
|
44
|
+
value_chain.each { |value|
|
45
|
+
add_value(prev_value, value)
|
46
|
+
prev_value = value
|
47
|
+
}
|
48
|
+
|
49
|
+
compute_frequencies
|
50
|
+
end
|
51
|
+
|
52
|
+
###################
|
53
|
+
private
|
54
|
+
###################
|
55
|
+
|
56
|
+
# Add a state on the transitions hash
|
57
|
+
def add_value(prev_value, value)
|
58
|
+
@transitions[prev_value] ||= Hash.new{0}
|
59
|
+
@transitions[prev_value][value] += 1
|
60
|
+
end
|
61
|
+
|
62
|
+
# Pick a value on the frequencies hash based on a random number and the previous state
|
63
|
+
def pick_value(random_number, prev_value)
|
64
|
+
next_value = @frequencies[prev_value]
|
65
|
+
if next_value.nil? then
|
66
|
+
next_value = @frequencies[nil]
|
67
|
+
end
|
68
|
+
|
69
|
+
succ_list = next_value.sort_by{|key, value| value}
|
70
|
+
freq_counter = 0.0
|
71
|
+
|
72
|
+
succ_list.each { |succ_value, freq|
|
73
|
+
freq_counter += freq
|
74
|
+
return succ_value if freq_counter >= random_number
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
# Compute the frequencies hash based on the transistions hash
|
79
|
+
def compute_frequencies
|
80
|
+
@transitions.map { |value, transition_hash|
|
81
|
+
sum = 0.0
|
82
|
+
transition_hash.each { |succ_value, occurencies|
|
83
|
+
sum += occurencies
|
84
|
+
}
|
85
|
+
|
86
|
+
transition_hash.each { |succ_value, occurencies|
|
87
|
+
@frequencies[value] ||= Hash.new{0}
|
88
|
+
@frequencies[value][succ_value] = occurencies/sum
|
89
|
+
}
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_s
|
94
|
+
return @frequencies
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'midilib/sequence'
|
2
|
+
require 'midilib/io/seqreader'
|
3
|
+
|
4
|
+
module Musikov
|
5
|
+
|
6
|
+
class FileNotFoundError < StandardError ; end
|
7
|
+
|
8
|
+
# This class is responsible for interacting with MidiLib in order
|
9
|
+
# to read the input midi files.
|
10
|
+
class MidiParser
|
11
|
+
|
12
|
+
####################
|
13
|
+
public
|
14
|
+
####################
|
15
|
+
|
16
|
+
# Initializes the parser using the file (or folder) path parameter
|
17
|
+
# * Parameter can be a single file path or a folder
|
18
|
+
def initialize(file_or_folder_paths = [])
|
19
|
+
@paths = []
|
20
|
+
@paths += file_or_folder_paths
|
21
|
+
end
|
22
|
+
|
23
|
+
# Obtains the list of midi files to parse and call the Midilib parse routine
|
24
|
+
def parse
|
25
|
+
result = []
|
26
|
+
files = []
|
27
|
+
|
28
|
+
@paths.each { |path|
|
29
|
+
begin
|
30
|
+
raise FileNotFoundError unless File.exists?(path)
|
31
|
+
|
32
|
+
if File.directory?(path) then
|
33
|
+
files += Dir.glob("#{path}/**/*.mid")
|
34
|
+
else
|
35
|
+
files << path if File.extname(path) == ".mid"
|
36
|
+
end
|
37
|
+
rescue
|
38
|
+
puts "Not a valid file path : #{path} => #{$!}"
|
39
|
+
end
|
40
|
+
}
|
41
|
+
|
42
|
+
|
43
|
+
if files.empty? then
|
44
|
+
puts "No files were added."
|
45
|
+
else
|
46
|
+
files.each { |file_path|
|
47
|
+
result << read_midi(file_path)
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
return result
|
52
|
+
end
|
53
|
+
|
54
|
+
####################
|
55
|
+
private
|
56
|
+
####################
|
57
|
+
|
58
|
+
# Call the Midilib's parse routine to read information from a midi file
|
59
|
+
def read_midi(file_path)
|
60
|
+
# Create a new, empty sequence.
|
61
|
+
seq = MIDI::Sequence.new()
|
62
|
+
|
63
|
+
# Read the contents of a MIDI file into the sequence.
|
64
|
+
File.open(file_path, 'rb') { | file |
|
65
|
+
seq.read(file)
|
66
|
+
}
|
67
|
+
|
68
|
+
return seq
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'midilib/sequence'
|
2
|
+
require 'midilib/consts'
|
3
|
+
require 'midilib/io/seqwriter'
|
4
|
+
|
5
|
+
module Musikov
|
6
|
+
|
7
|
+
# This class is responsible for interacting with MidiLib in order
|
8
|
+
# to write the output midi files.
|
9
|
+
class MidiWriter
|
10
|
+
|
11
|
+
####################
|
12
|
+
public
|
13
|
+
####################
|
14
|
+
|
15
|
+
# Initializes the parser using the output path parameter
|
16
|
+
# * Parameter need to be a folder folder
|
17
|
+
def initialize(output_path = ".")
|
18
|
+
@path = output_path
|
19
|
+
end
|
20
|
+
|
21
|
+
# Writes the output midi file from the generated sequence hash
|
22
|
+
def write(sequence_hash)
|
23
|
+
# Create a new, empty sequence.
|
24
|
+
seq = MIDI::Sequence.new()
|
25
|
+
|
26
|
+
track = MIDI::Track.new(seq)
|
27
|
+
seq.tracks << track
|
28
|
+
track.events << MIDI::Tempo.new(MIDI::Tempo.bpm_to_mpq(120))
|
29
|
+
i = 0
|
30
|
+
|
31
|
+
# Create a track to hold the notes. Add it to the sequence.
|
32
|
+
sequence_hash.each { |program_change, midi_elements|
|
33
|
+
track = MIDI::Track.new(seq)
|
34
|
+
seq.tracks << track
|
35
|
+
|
36
|
+
instrument = MIDI::GM_PATCH_NAMES[program_change]
|
37
|
+
|
38
|
+
# Give the track a name and an instrument name (optional).
|
39
|
+
track.instrument = instrument
|
40
|
+
|
41
|
+
# Add a volume controller event (optional).
|
42
|
+
track.events << MIDI::Controller.new(i, MIDI::CC_VOLUME, 127)
|
43
|
+
track.events << MIDI::ProgramChange.new(i, program_change, 0)
|
44
|
+
midi_elements.each { |midi_element|
|
45
|
+
track.events << MIDI::NoteOn.new(i, midi_element.note ,127,0)
|
46
|
+
track.events << MIDI::NoteOff.new(i, midi_element.note ,127, seq.note_to_delta(midi_element.duration))
|
47
|
+
}
|
48
|
+
|
49
|
+
i += 1
|
50
|
+
}
|
51
|
+
|
52
|
+
write_midi(seq)
|
53
|
+
end
|
54
|
+
|
55
|
+
####################
|
56
|
+
private
|
57
|
+
####################
|
58
|
+
|
59
|
+
# Call the Midilib's parse routine to read information from a midi file
|
60
|
+
def write_midi(seq)
|
61
|
+
# Writing output file
|
62
|
+
File.open("#{@path}/output.mid", 'wb') { | file | seq.write(file) }
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'musikov/markov_builder'
|
3
|
+
|
4
|
+
describe Musikov::MidiElement do
|
5
|
+
|
6
|
+
it "should respect equals based on the 'note' and 'duration' attributes" do
|
7
|
+
midi1 = Musikov::MidiElement.new("silence", 64)
|
8
|
+
midi2 = Musikov::MidiElement.new("silence", 64)
|
9
|
+
|
10
|
+
midi1.should == midi2
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should same hash if objects have the same attributes" do
|
14
|
+
midi1 = Musikov::MidiElement.new("silence", 64)
|
15
|
+
midi2 = Musikov::MidiElement.new("silence", 64)
|
16
|
+
|
17
|
+
midi1.hash.should == midi2.hash
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should respect eql? equality" do
|
21
|
+
midi1 = Musikov::MidiElement.new("silence", 64)
|
22
|
+
midi2 = Musikov::MidiElement.new("silence", 64)
|
23
|
+
|
24
|
+
(midi1.eql? midi2).should == true
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'musikov/markov_model'
|
3
|
+
|
4
|
+
describe Musikov::MarkovModel do
|
5
|
+
|
6
|
+
it "should add values with its correspondent frequence" do
|
7
|
+
text = "The man is tall. The man is big."
|
8
|
+
|
9
|
+
mc = Musikov::MarkovModel.new(text.split)
|
10
|
+
mc.frequencies["is"].should == {"big." => 0.5, "tall." => 0.5}
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should pick word corresponding to random number" do
|
14
|
+
text = "The man is tall. The man is big."
|
15
|
+
|
16
|
+
# Obs: testing private method through "send"
|
17
|
+
mc = Musikov::MarkovModel.new(text.split)
|
18
|
+
wd = mc.send(:pick_value, 0.8, "is")
|
19
|
+
wd.should == "tall."
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should re-compute frequencies after inserting more input" do
|
23
|
+
text1 = "The man is tall. The man is big."
|
24
|
+
text2 = "The man is strange. The man is very strange."
|
25
|
+
|
26
|
+
mc = Musikov::MarkovModel.new()
|
27
|
+
mc.add_input(text1.split)
|
28
|
+
mc.frequencies["is"].should == {"big." => 0.5, "tall." => 0.5}
|
29
|
+
|
30
|
+
mc.add_input(text2.split)
|
31
|
+
mc.frequencies["is"].should == {"big." => 0.25, "tall." => 0.25, "strange." => 0.25, "very" => 0.25}
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: musikov
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.15'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Andre Fonseca
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-22 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: midilib
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: thor
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: Musikov - Random song generator based on Markov Chains
|
47
|
+
email: andre.amorimfonseca@gmail.com
|
48
|
+
executables:
|
49
|
+
- musikov
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files:
|
52
|
+
- LICENSE
|
53
|
+
- README.md
|
54
|
+
files:
|
55
|
+
- LICENSE
|
56
|
+
- README.md
|
57
|
+
- bin/musikov
|
58
|
+
- lib/musikov/markov_builder.rb
|
59
|
+
- lib/musikov/markov_model.rb
|
60
|
+
- lib/musikov/midi_parser.rb
|
61
|
+
- lib/musikov/midi_writer.rb
|
62
|
+
- lib/musikov.rb
|
63
|
+
- spec/markov_model_spec.rb
|
64
|
+
- spec/markov_builder_spec.rb
|
65
|
+
homepage: https://github.com/andreAmorimF/musikov
|
66
|
+
licenses: []
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options: []
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
requirements: []
|
84
|
+
rubyforge_project:
|
85
|
+
rubygems_version: 1.8.24
|
86
|
+
signing_key:
|
87
|
+
specification_version: 3
|
88
|
+
summary: Musikov - Random song generator based on Markov Chains
|
89
|
+
test_files: []
|