469 lines
8.7 KiB
Ruby
469 lines
8.7 KiB
Ruby
#!/usr/bin/env ruby
|
|
#
|
|
# JavaScript Soundfont Builder for MIDI.js
|
|
# Author: 0xFE <mohit@muthanna.com>
|
|
# edited by Valentijn Nieman <valentijnnieman@gmail.com>
|
|
#
|
|
# Requires:
|
|
#
|
|
# FluidSynth
|
|
# Lame
|
|
# Ruby Gems: midilib parallel
|
|
#
|
|
# $ brew install fluidsynth lame (on OSX)
|
|
# $ gem install midilib parallel
|
|
#
|
|
# You'll need to download a GM soundbank to generate audio.
|
|
#
|
|
# Usage:
|
|
#
|
|
# 1) Install the above dependencies.
|
|
# 2) Edit BUILD_DIR, SOUNDFONT, and INSTRUMENTS as required.
|
|
# 3) Run without any argument.
|
|
|
|
require 'base64'
|
|
require 'digest/sha1'
|
|
require 'etc'
|
|
require 'fileutils'
|
|
require 'midilib'
|
|
require 'parallel'
|
|
require 'zlib'
|
|
require 'json'
|
|
|
|
include FileUtils
|
|
|
|
BUILD_DIR = "./sound-font" # Output path
|
|
SOUNDFONT = "./default_sound_font.sf2" # Soundfont file path
|
|
|
|
# This script will generate MIDI.js-compatible instrument JS files for
|
|
# all instruments in the below array. Add or remove as necessary.
|
|
INSTRUMENTS = [
|
|
0,
|
|
1,
|
|
2,
|
|
3,
|
|
4,
|
|
5,
|
|
6,
|
|
7,
|
|
8,
|
|
9,
|
|
10,
|
|
11,
|
|
12,
|
|
13,
|
|
14,
|
|
15,
|
|
16,
|
|
17,
|
|
18,
|
|
19,
|
|
20,
|
|
21,
|
|
22,
|
|
23,
|
|
24,
|
|
25,
|
|
26,
|
|
27,
|
|
28,
|
|
29,
|
|
30,
|
|
31,
|
|
32,
|
|
33,
|
|
34,
|
|
35,
|
|
36,
|
|
37,
|
|
38,
|
|
39,
|
|
40,
|
|
41,
|
|
42,
|
|
43,
|
|
44,
|
|
45,
|
|
46,
|
|
47,
|
|
48,
|
|
49,
|
|
50,
|
|
51,
|
|
52,
|
|
53,
|
|
54,
|
|
55,
|
|
56,
|
|
57,
|
|
58,
|
|
59,
|
|
60,
|
|
61,
|
|
62,
|
|
63,
|
|
64,
|
|
65,
|
|
66,
|
|
67,
|
|
68,
|
|
69,
|
|
70,
|
|
71,
|
|
72,
|
|
73,
|
|
74,
|
|
75,
|
|
76,
|
|
77,
|
|
78,
|
|
79,
|
|
80,
|
|
81,
|
|
82,
|
|
83,
|
|
84,
|
|
85,
|
|
86,
|
|
87,
|
|
88,
|
|
89,
|
|
90,
|
|
91,
|
|
92,
|
|
93,
|
|
94,
|
|
95,
|
|
96,
|
|
97,
|
|
98,
|
|
99,
|
|
100,
|
|
101,
|
|
102,
|
|
103,
|
|
104,
|
|
105,
|
|
106,
|
|
107,
|
|
108,
|
|
109,
|
|
110,
|
|
111,
|
|
112,
|
|
113,
|
|
114,
|
|
115,
|
|
116,
|
|
117,
|
|
118,
|
|
119,
|
|
120,
|
|
121,
|
|
122,
|
|
123,
|
|
124,
|
|
125,
|
|
126,
|
|
127
|
|
]
|
|
|
|
# It was found that midilib uses names that are incompatible with MIDI.js
|
|
# For example, midilib uses "SynthBrass 1" -> https://github.com/jimm/midilib/blob/6c8e481ae72cd9f00a38eb3700ddfca6b549f153/lib/midilib/consts.rb#L280
|
|
# and the MIDI association uses "SynthBrass 1" -> https://www.midi.org/specifications-old/item/gm-level-1-sound-set
|
|
# but the MIDI.js calls this "Synth Brass 1" -> https://github.com/mudcube/MIDI.js/blob/a8a84257afa70721ae462448048a87301fc1554a/js/midi/gm.js#L44
|
|
# there are others like "Bag pipe" vs "Bagpipe", etc.
|
|
# here, we use the MIDI.js definitions because that is how most users will interact with the generated soundfonts.
|
|
MIDIJS_PATCH_NAMES = [
|
|
"Acoustic Grand Piano",
|
|
"Bright Acoustic Piano",
|
|
"Electric Grand Piano",
|
|
"Honky-tonk Piano",
|
|
"Electric Piano 1",
|
|
"Electric Piano 2",
|
|
"Harpsichord",
|
|
"Clavinet",
|
|
"Celesta",
|
|
"Glockenspiel",
|
|
"Music Box",
|
|
"Vibraphone",
|
|
"Marimba",
|
|
"Xylophone",
|
|
"Tubular Bells",
|
|
"Dulcimer",
|
|
"Drawbar Organ",
|
|
"Percussive Organ",
|
|
"Rock Organ",
|
|
"Church Organ",
|
|
"Reed Organ",
|
|
"Accordion",
|
|
"Harmonica",
|
|
"Tango Accordion",
|
|
"Acoustic Guitar (nylon)",
|
|
"Acoustic Guitar (steel)",
|
|
"Electric Guitar (jazz)",
|
|
"Electric Guitar (clean)",
|
|
"Electric Guitar (muted)",
|
|
"Overdriven Guitar",
|
|
"Distortion Guitar",
|
|
"Guitar Harmonics",
|
|
"Acoustic Bass",
|
|
"Electric Bass (finger)",
|
|
"Electric Bass (pick)",
|
|
"Fretless Bass",
|
|
"Slap Bass 1",
|
|
"Slap Bass 2",
|
|
"Synth Bass 1",
|
|
"Synth Bass 2",
|
|
"Violin",
|
|
"Viola",
|
|
"Cello",
|
|
"Contrabass",
|
|
"Tremolo Strings",
|
|
"Pizzicato Strings",
|
|
"Orchestral Harp",
|
|
"Timpani",
|
|
"String Ensemble 1",
|
|
"String Ensemble 2",
|
|
"Synth Strings 1",
|
|
"Synth Strings 2",
|
|
"Choir Aahs",
|
|
"Voice Oohs",
|
|
"Synth Choir",
|
|
"Orchestra Hit",
|
|
"Trumpet",
|
|
"Trombone",
|
|
"Tuba",
|
|
"Muted Trumpet",
|
|
"French Horn",
|
|
"Brass Section",
|
|
"Synth Brass 1",
|
|
"Synth Brass 2",
|
|
"Soprano Sax",
|
|
"Alto Sax",
|
|
"Tenor Sax",
|
|
"Baritone Sax",
|
|
"Oboe",
|
|
"English Horn",
|
|
"Bassoon",
|
|
"Clarinet",
|
|
"Piccolo",
|
|
"Flute",
|
|
"Recorder",
|
|
"Pan Flute",
|
|
"Blown Bottle",
|
|
"Shakuhachi",
|
|
"Whistle",
|
|
"Ocarina",
|
|
"Lead 1 (square)",
|
|
"Lead 2 (sawtooth)",
|
|
"Lead 3 (calliope)",
|
|
"Lead 4 (chiff)",
|
|
"Lead 5 (charang)",
|
|
"Lead 6 (voice)",
|
|
"Lead 7 (fifths)",
|
|
"Lead 8 (bass + lead)",
|
|
"Pad 1 (new age)",
|
|
"Pad 2 (warm)",
|
|
"Pad 3 (polysynth)",
|
|
"Pad 4 (choir)",
|
|
"Pad 5 (bowed)",
|
|
"Pad 6 (metallic)",
|
|
"Pad 7 (halo)",
|
|
"Pad 8 (sweep)",
|
|
"FX 1 (rain)",
|
|
"FX 2 (soundtrack)",
|
|
"FX 3 (crystal)",
|
|
"FX 4 (atmosphere)",
|
|
"FX 5 (brightness)",
|
|
"FX 6 (goblins)",
|
|
"FX 7 (echoes)",
|
|
"FX 8 (sci-fi)",
|
|
"Sitar",
|
|
"Banjo",
|
|
"Shamisen",
|
|
"Koto",
|
|
"Kalimba",
|
|
"Bagpipe",
|
|
"Fiddle",
|
|
"Shanai",
|
|
"Tinkle Bell",
|
|
"Agogo",
|
|
"Steel Drums",
|
|
"Woodblock",
|
|
"Taiko Drum",
|
|
"Melodic Tom",
|
|
"Synth Drum",
|
|
"Reverse Cymbal",
|
|
"Guitar Fret Noise",
|
|
"Breath Noise",
|
|
"Seashore",
|
|
"Bird Tweet",
|
|
"Telephone Ring",
|
|
"Helicopter",
|
|
"Applause",
|
|
"Gunshot"
|
|
]
|
|
|
|
# The encoders and tools are expected in your PATH. You can supply alternate
|
|
# paths by changing the constants below.
|
|
LAME = "lame" # `which lame`.chomp
|
|
FLUIDSYNTH = "fluidsynth" # `which fluidsynth`.chomp
|
|
|
|
puts "Building the following instruments using font: " + SOUNDFONT
|
|
|
|
# Display instrument names.
|
|
INSTRUMENTS.each do |i|
|
|
puts " #{i}: " + MIDIJS_PATCH_NAMES[i]
|
|
end
|
|
|
|
puts
|
|
puts "Using MP3 encoder: " + LAME
|
|
puts "Using FluidSynth encoder: " + FLUIDSYNTH
|
|
puts
|
|
puts "Sending output to: " + BUILD_DIR
|
|
puts
|
|
|
|
raise "Can't find soundfont: #{SOUNDFONT}" unless File.exist? SOUNDFONT
|
|
raise "Can't find 'lame' command" if LAME.empty?
|
|
raise "Can't find 'fluidsynth' command" if FLUIDSYNTH.empty?
|
|
raise "Output directory does not exist: #{BUILD_DIR}" unless File.exist?(BUILD_DIR)
|
|
|
|
puts "Hit return to begin."
|
|
$stdin.readline
|
|
|
|
NOTES = {
|
|
"C" => 0,
|
|
"Db" => 1,
|
|
"D" => 2,
|
|
"Eb" => 3,
|
|
"E" => 4,
|
|
"F" => 5,
|
|
"Gb" => 6,
|
|
"G" => 7,
|
|
"Ab" => 8,
|
|
"A" => 9,
|
|
"Bb" => 10,
|
|
"B" => 11
|
|
}
|
|
|
|
MIDI_C0 = 12
|
|
VELOCITY = 100
|
|
DURATION = Integer(3000)
|
|
TEMP_FILE = "#{BUILD_DIR}/%s%stemp.midi"
|
|
FLUIDSYNTH_RAW = "%s.wav"
|
|
|
|
def deflate(string, level)
|
|
z = Zlib::Deflate.new(level)
|
|
dst = z.deflate(string, Zlib::FINISH)
|
|
z.close
|
|
dst
|
|
end
|
|
|
|
def note_to_int(note, octave)
|
|
value = NOTES[note]
|
|
increment = MIDI_C0 * octave
|
|
return value + increment
|
|
end
|
|
|
|
def int_to_note(value)
|
|
raise "Bad Value" if value < MIDI_C0
|
|
reverse_notes = NOTES.invert
|
|
value -= MIDI_C0
|
|
octave = value / 12
|
|
note = value % 12
|
|
return { key: reverse_notes[note],
|
|
octave: octave }
|
|
end
|
|
|
|
# Run a quick table validation
|
|
MIDI_C0.upto(100) do |x|
|
|
note = int_to_note x
|
|
#raise "Broken table" unless note_to_int(note[:key], note[:octave]) == x
|
|
end
|
|
|
|
def generate_midi(program, note_value, file)
|
|
include MIDI
|
|
seq = Sequence.new()
|
|
track = Track.new(seq)
|
|
|
|
seq.tracks << track
|
|
track.events << ProgramChange.new(0, Integer(program))
|
|
track.events << NoteOn.new(0, note_value, VELOCITY, 0) # channel, note, velocity, delta
|
|
track.events << NoteOff.new(0, note_value, VELOCITY, DURATION)
|
|
|
|
File.open(file, 'wb') { | file | seq.write(file) }
|
|
end
|
|
|
|
def run_command(cmd)
|
|
puts "Running: " + cmd
|
|
`#{cmd}`
|
|
end
|
|
|
|
def midi_to_audio(source, target)
|
|
run_command "#{FLUIDSYNTH} -C no -R no -g 0.5 -F #{target} #{SOUNDFONT} #{source}"
|
|
run_command "#{LAME} -v -b 8 -B 64 #{target}"
|
|
rm target
|
|
end
|
|
|
|
def open_js_file(instrument_key, type)
|
|
js_file = File.open("#{BUILD_DIR}/#{instrument_key}-#{type}.js", "w")
|
|
js_file.write(
|
|
"""
|
|
if (typeof(MIDI) === 'undefined') var MIDI = {};
|
|
if (typeof(MIDI.Soundfont) === 'undefined') MIDI.Soundfont = {};
|
|
MIDI.Soundfont.#{instrument_key} = {
|
|
""")
|
|
return js_file
|
|
end
|
|
|
|
def close_js_file(file)
|
|
file.write("\n}\n")
|
|
file.close
|
|
end
|
|
|
|
def base64js(note, file, type)
|
|
output = '"' + note + '": '
|
|
output += '"' + "data:audio/#{type};base64,"
|
|
output += Base64.strict_encode64(File.read(file)) + '"'
|
|
return output
|
|
end
|
|
|
|
def generate_audio(program)
|
|
instrument = MIDIJS_PATCH_NAMES[program]
|
|
instrument_key = instrument.downcase.gsub(/[^a-z0-9 ]/, "").gsub(/[ ]/, "_")
|
|
|
|
puts "Generating audio for: " + instrument + "(#{instrument_key})"
|
|
|
|
mkdir_p "#{BUILD_DIR}/#{instrument_key}"
|
|
|
|
|
|
note_to_int("A", 0).upto(note_to_int("C", 8)) do |note_value|
|
|
output_name = "p#{note_value}_v#{VELOCITY}"
|
|
output_path_prefix = BUILD_DIR + "/#{instrument_key}" + output_name
|
|
|
|
puts "Generating: #{output_name}"
|
|
temp_file_specific = TEMP_FILE % [output_name, instrument_key]
|
|
generate_midi(program, note_value, temp_file_specific)
|
|
midi_to_audio(temp_file_specific, output_path_prefix + ".wav")
|
|
|
|
mv output_path_prefix + ".mp3", "#{BUILD_DIR}/#{instrument_key}/#{output_name}.mp3"
|
|
rm temp_file_specific
|
|
end
|
|
|
|
tempHash = {
|
|
"name" => instrument_key,
|
|
"minPitch" => 0,
|
|
"maxPitch" => 127,
|
|
"durationSeconds" => 3.0,
|
|
"releaseSeconds" => 1.0,
|
|
"percussive": false,
|
|
"velocities": [100]
|
|
}
|
|
|
|
File.open("#{BUILD_DIR}/#{instrument_key}/instrument.json", "w") do |f|
|
|
f.write(tempHash.to_json)
|
|
end
|
|
end
|
|
|
|
Parallel.each(INSTRUMENTS, :in_processes=>Etc.nprocessors){|i| generate_audio(i)} |