#!/usr/bin/env ruby # # JavaScript Soundfont Builder for MIDI.js # Author: 0xFE # edited by Valentijn Nieman # # 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)}