Skip to content

Instantly share code, notes, and snippets.

@basicfeatures
Last active May 17, 2023 17:19
Show Gist options
  • Save basicfeatures/d3332e55aef9de036e777c51d06d144f to your computer and use it in GitHub Desktop.
Save basicfeatures/d3332e55aef9de036e777c51d06d144f to your computer and use it in GitHub Desktop.

We the first to manage proper audio-to-MIDI conversion?

  • The first Ruby script uses Spotify's Basic Pitch to reverse engineer piano chord videos from YouTube into MIDI and the result is horrible
  • The second uses, as first in the world, Midilib and Chords2Midi to quantize and solidify all possible chord progressions and then give off a live preview: https://wavefilegem.com/

get_midi.rb

# encoding: UTF-8
#
# MASS-CONVERT AUDIO TO MIDI
# https://replicate.com/rhelsing/basic-pitch
#
# Download piano chord lessons
# yt-dlp -x --audio-format mp3 https://www.youtube.com/@PianOwnedLessons
#
# gem install --user-install replicate-ruby
# gem install --user-install midilib

require 'replicate'
require 'midilib'
require 'fileutils'
require 'base64'
require 'pry'

Replicate.configure do |config|
  config.api_token = 'r8_L77Bahb1EsOdAQGFiujQ5sZWTSCCv063QX8Yq'
end

model = Replicate.client.retrieve_model('rhelsing/basic-pitch')
version = model.latest_version

Dir.glob('samples/*.mp3').each_with_index do |mp3_file, index|
  puts "Processing file #{index + 1} of #{mp3_file.length}: #{mp3_file}"

  puts "Uploading #{mp3_file}..."
  upload = Replicate.client.create_upload(audio_file: File.open(mp3_file))

  puts "Waiting for upload to finish..."
  loop do
    break unless upload == false
    sleep 10
  end

  puts "Creating prediction..."
  prediction = Replicate.client.create_prediction(
    input: {
      audio_file: upload
    }
  )

  puts "Waiting for prediction to finish..."

  loop do
    prediction.refetch

    if prediction.status == "succeeded"
      output = prediction.output

      if output && output.key?("midi_data")
        # Extract the MIDI data from the output
        midi_data = output['midi_data']

        # Decode the Base64-encoded MIDI data
        decoded_midi_data = Base64.strict_decode64(midi_data)

        # Specify the output file path
        output_file = "output/#{File.basename(mp3_file, '.mp3')}.mid"

        # Save the MIDI data to the output file
        File.open(output_file, 'wb') do |file|
          file.write(decoded_midi_data)
        end

        puts "MIDI file saved: #{output_file}"
        break
      end
    elsif prediction.status == "failed"
      puts "Prediction failed."
      exit
    end

    sleep 30
  end
end

improve_midi.rb

# encoding: UTF-8
#
# CONVERTS TO REAL CHORDS
#
# gem install --user-install replicate-ruby midilib
# gem install --user-install replicate-ruby pycall
# gem install --user-install replicate-ruby wavefile
#
# doas pkg_add python3 py3-pip
# git clone https://github.com/Miserlou/chords2midi
# pip3 install ./chords2midi

require 'replicate'
require 'midilib'
require 'fileutils'
require 'pycall/import'
require 'wavefile'
require 'pry'

include PyCall::Import
pyimport :chords2midi

def improve_midi(midi_file)
  puts "Improving MIDI file: #{midi_file}"

  sequence = MIDI::Sequence.new()

  File.open(midi_file, 'rb') do |file|
    sequence.read(file)
  end

  # Quantize
  sequence.each do |track|
    track.quantize(4) # Adjust the quantize resolution as needed
  end

  # Chordify
  chord_converter = Chords2MIDI.new
  begin
    chord_converter.process(sequence)
  rescue => e
    puts "Chords2MIDI processing failed for #{midi_file}: #{e.message}"
    return
  end

  processed_midi_file = midi_file.sub(/\.mid$/, '_processed.mid')

  File.open(processed_midi_file, 'wb') do |file|
    sequence.write(file)
  end

  puts "Processed MIDI file saved: #{processed_midi_file}"

  # Prompt the user for save or delete
  loop do
    puts "Do you want to save or delete the processed MIDI file? (yes/no)"
    response = gets.chomp.downcase

    case response
    when 'yes'
      play_midi(processed_midi_file)
      break
    when 'no'
      delete_file(processed_midi_file)
      break
    else
      puts "Invalid response. Please enter 'yes' or 'no'."
    end
  end
end

# Let's hear it
def play_midi(midi_file)
  puts "Playing MIDI file: #{midi_file}"

  output = WaveFile::Writer.new("output.wav", WaveFile::Format.new(:mono, :pcm_16, 44100))
  sequence = MIDI::Sequence.new()

  File.open(midi_file, 'rb') do |file|
    sequence.read(file)
  end

  synth = MIDI::Synthesizer.new()
  synth.open

  sequence.each do |track|
    track.each do |event|
      sleep(event.time_from_start - synth.current_time) if event.time_from_start > synth.current_time

      case event
      when MIDI::NoteOn
        synth.note_on(event.note, event.velocity, event.channel)
      when MIDI::NoteOff
        synth.note_off(event.note, event.channel)
      end
    end
  end

  synth.close
  output.close

  system("aplay output.wav")  # Adjust this command based on your system's audio playback command
  File.delete("output.wav")
end

def delete_file(file)
  File.delete(file)
  puts "Processed MIDI file deleted: #{file}"
end

Dir.glob(File.join('samples/', '*.mid')) do |midi_file|
  puts "Processing #{midi_file}..."
  improve_midi(midi_file)
  puts "Processing complete for #{midi_file}."
end

puts "All done. Exiting."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment