musikov 0.15
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.
- 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: []
|