Skip to content

Instantly share code, notes, and snippets.

@siers
Last active December 22, 2015 04:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save siers/6418475 to your computer and use it in GitHub Desktop.
Save siers/6418475 to your computer and use it in GitHub Desktop.
Creates a MIDI file with chords, declared by a cryptic chord syntax.
#!/usr/bin/env ruby
# self(44aa 78ea 455d), /03.09.2013./
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
require 'bundler'
Bundler.require
class Chord
attr_accessor :base, :name, :minor, :position, :nths, :octave, :shifts
def notes
@@notes ||= %w"i ii iii iv v vi vii".zip([0, 2, 4, 5, 7, 9, 11])
end
def hnotes
@@hnotes ||= Hash[notes]
end
def parse
syntax = /([b#])?(#{ hnotes.keys.join('|') })(\d+)?(o?)([<>]+)?$/i
_, @offset, @position, @nths, @octave, @shifts = name.match(syntax).to_a
@octave = @octave == "" ? nil : @octave
@minor = name == name.downcase
end
def initialize(base, name)
@name = name
parse
@base = base
@base += hnotes[@position.downcase]
@base += {'b' => -1, '#' => 1}[@offset].to_i
end
def rotate(harmonies)
return harmonies unless shifts
shifts.each_char do |shift|
# > == up, < == down
settings = (shift == '>') ? [1, -1, 12] : [-1, 0, -12] # direction, index, offset
harmonies.rotate!(settings[0])
harmonies[settings[1]] += settings[2]
end
harmonies
end
def oddities(harmonies)
additional = {7 => 1, 9 => 2, 11 => 3}[nths.to_i].to_i
octaves = octave ? 1 : 0
harmonies.first(3 + additional + octaves)
end
def to_a
level rotate oddities harmonies
end
private
def harmonies
if minor
[octave ? 12 : nil, 0, 3, 7, 10, 14, 16]
else
[octave ? 12 : nil, 0, 4, 7, 11, 14, 16]
end.compact
end
def level(harmonies) # as a verb
harmonies.map(&base.method(:+))
end
end
class HarmonyPump
attr_accessor :name, :base, :chords, :track, :seq
include MIDI
def initialize(base, *notes)
@base = base
@name = notes.join('-')
@chords = notes.map(&method(:chord))
@seq = Sequence.new
populate_tracks
end
def chord(name)
Chord.new(base, name)
end
def populate_tracks
strack = Track.new(seq) # Settings track.
seq.tracks << strack
strack.events << Tempo.new(Tempo.bpm_to_mpq(120))
strack.events << MetaEvent.new(META_SEQ_NAME, name)
@track = Track.new(seq)
seq.tracks << track
track.name = name
track.instrument = GM_PATCH_NAMES[7]
track.events << Controller.new(0, CC_VOLUME, 255)
end
def length(chords, nth)
len = seq.note_to_delta('half')
if chords.count == nth + 1
len * 3
else
len
end
end
def notes
# note.new(chan, note, velocity, delta_time)
track.events << ProgramChange.new(0, 1, 0)
chords.each_with_index do |chord, chord_i|
notes = chord.to_a
notes.each do |note|
track.events << NoteOn .new(0, note, 127, 0)
end
notes.each_with_index do |note, i|
track.events << NoteOff.new(0, note, 127, i == 0 ? length(chords, chord_i) : 0)
end
end
end
def >>(io)
notes
seq.write(io)
end
def debug
chords.each do |chord|
$stderr.write "Interpreted #{ chord.name } as:\t#{ chord.to_a }\n"
end
end
end
HarmonyPump.new(45, *%w"I bIII< bVIo<< bII7 I").tap(&:debug) >> $>
@siers
Copy link
Author

siers commented Sep 3, 2013

"midilib" gem is required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment