Last active
October 12, 2023 12:41
-
-
Save Guseyn/2946a3dd502f7168ca8d9c7e24b67f81 to your computer and use it in GitHub Desktop.
generate-magenta-sound-font
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# | |
# JavaScript Soundfont Builder for MIDI.js | |
# Author: 0xFE <[email protected]> | |
# edited by Valentijn Nieman <[email protected]> | |
# | |
# 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 = "./web-app/magenta-soundfonts/SGM" # Output path | |
SOUNDFONT = "./soundfonts/SGM.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 = `which lame`.chomp | |
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.exists? 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.exists?(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 | |
VELOCITIES = [ | |
20, 25, 30, 35, 40, 45, 47, 50, | |
55, 57, 60, 65, 70, 75, 80, 85, | |
90, 95, 100, 105, 110, 112, 115, | |
117, 120, 122, 125, 127 | |
] | |
DURATION = Integer(4500) | |
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, velocity, 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 -q -F #{target} #{SOUNDFONT} #{source}" | |
run_command "#{LAME} -v -b 8 -B 64 #{target} &>/dev/null" | |
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| | |
VELOCITIES.each do |velocity| | |
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, velocity, 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 | |
puts "instrument: #{instrument_key} finished for note #{note_value} for velocity #{velocity}" | |
end | |
puts "instrument: #{instrument_key} finished for note #{note_value} for all velocities" | |
end | |
tempHash = { | |
"name" => instrument_key, | |
"minPitch" => 0, | |
"maxPitch" => 127, | |
"durationSeconds" => 4.5, | |
"releaseSeconds" => 1.0, | |
"percussive": false, | |
"velocities": VELOCITIES | |
} | |
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)} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment