Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Python 3 #8

Open
justinsalamon opened this issue Oct 30, 2018 · 9 comments
Open

Add support for Python 3 #8

justinsalamon opened this issue Oct 30, 2018 · 9 comments

Comments

@justinsalamon
Copy link
Owner

No description provided.

@ucasiggcas
Copy link

I have the same question.
We also need python3 support !

@DavidParkin
Copy link

What is broken with Python 3? I've just used it.

@hpx7
Copy link

hpx7 commented May 7, 2019

It doesn't seem to work with Python3:

Loading audio...
Extracting melody f0 with MELODIA...
Converting Hz to MIDI notes...
Traceback (most recent call last):
  File "audio_to_midi_melodia.py", line 225, in <module>
    savejams=args.jams)
  File "audio_to_midi_melodia.py", line 191, in audio_to_midi_melodia
    notes = midi_to_notes(midi_pitch, fs, hop, smooth, minduration)
  File "audio_to_midi_melodia.py", line 115, in midi_to_notes
    if p_prev > 0:
TypeError: '>' not supported between instances of 'NoneType' and 'int'

@justinsalamon
Copy link
Owner Author

This specific error is described in #5. Fixing this issue seems straight forward, full python 3 support might require a few more changes. I'll have time to look into python 3 support in about 1 month. In the meanwhile, the easiest solution is to create a python 2.7 environment (e.g. using miniconda) and run the script from within this environment. Thanks in advance for your patience!

@moziguang
Copy link

I fixed it as follow.
change
notes.append((onset_sec, duration_sec, p_prev))
to
notes.append((onset_sec, duration_sec, int(p_prev)))

@DavidParkin
Copy link

I changed the initialisation of p_prev (about line 107) to
p_prev = 0

@FoxesAreCute
Copy link

I just want to add, there are two instances of p_prev you need to change. Line 126, and 107. Otherwise you get the error specified here

The codes sound, no pun intended and it's honestly amazing how it works with such a newer version of python so easily. I wouldn't abandon this project, it has a LOT of potential for DJs, Parody makers, and samplers like me alike.

@Wattbag
Copy link

Wattbag commented Jun 3, 2023

Great code Justin 👍 & Great fix guys !
Set p_prev = 0 at line 107
Replace p_prev by int(p_prev) at lines 121 & 133 and it just works fine in Python 3 !! Awesome result !!

@duringleaves
Copy link

duringleaves commented Oct 17, 2024

I updated this to run under Python3. I'm not able to post it to github at the moment, but for anyone else looking to use it on modern Python, here's a quick rundown:

  • Open .venv/lib/python3.11/site-packages/jams/schema.py and find/replace all
    =np.float_
    to
    =np.float64

  • Instead of vamp install librosa

  • update the main .py file code to:


import soundfile
import resampy
import librosa
import argparse
import os
import numpy as np
from midiutil.MidiFile import MIDIFile
from scipy.signal import medfilt
import jams
import __init__

def save_jams(jamsfile, notes, track_duration, orig_filename):
    jam = jams.JAMS()
    jam.file_metadata.duration = track_duration
    jam.file_metadata.title = orig_filename

    midi_an = jams.Annotation(namespace='pitch_midi', duration=track_duration)
    midi_an.annotation_metadata = jams.AnnotationMetadata(
        data_source='audio_to_midi_melodia.py v%s' % __init__.__version__,
        annotation_tools='audio_to_midi_melodia.py')

    for n in notes:
        midi_an.append(time=n[0], duration=n[1], value=n[2], confidence=0)

    jam.annotations.append(midi_an)
    jam.save(jamsfile)

def save_midi(outfile, notes, tempo):
    track = 0
    time = 0
    midifile = MIDIFile(1)

    # Add track name and tempo.
    midifile.addTrackName(track, time, "MIDI TRACK")
    midifile.addTempo(track, time, tempo)

    channel = 0
    volume = 100

    for note in notes:
        onset = note[0] * (tempo/60.)
        duration = note[1] * (tempo/60.)
        pitch = int(note[2])  # Ensure pitch is an integer
        midifile.addNote(track, channel, pitch, onset, duration, volume)

    # And write it to disk.
    with open(outfile, 'wb') as binfile:
        midifile.writeFile(binfile)


def midi_to_notes(midi, fs, hop, smooth, minduration):
    if smooth > 0:
        filter_duration = smooth
        filter_size = int(filter_duration * fs / float(hop))
        if filter_size % 2 == 0:
            filter_size += 1
        midi_filt = medfilt(midi, filter_size)
    else:
        midi_filt = midi

    notes = []
    p_prev = 0
    duration = 0
    onset = 0

    for n, p in enumerate(midi_filt):
        # Ensure we only process valid pitch values (not None)
        if p is None:
            p = 0  # Treat None values as silence (MIDI 0)

        if p == p_prev:
            duration += 1
        else:
            if p_prev > 0:
                duration_sec = duration * hop / float(fs)
                if duration_sec >= minduration:
                    onset_sec = onset * hop / float(fs)
                    notes.append((onset_sec, duration_sec, p_prev))
            onset = n
            duration = 1
            p_prev = p

    if p_prev > 0:
        duration_sec = duration * hop / float(fs)
        onset_sec = onset * hop / float(fs)
        notes.append((onset_sec, duration_sec, int(p_prev)))

    return notes


def hz2midi(hz):
    hz_nonneg = hz.copy()
    idx = hz_nonneg <= 0
    hz_nonneg[idx] = 1
    midi = 69 + 12 * np.log2(hz_nonneg / 440.)
    midi[idx] = 0
    midi = np.round(midi)
    return midi

def audio_to_midi_melodia(infile, outfile, bpm, smooth=0.25, minduration=0.1, savejams=False):
    fs = 44100
    hop = 128

    # Load audio using librosa
    print("Loading audio...")
    data, sr = soundfile.read(infile)
    if len(data.shape) > 1 and data.shape[1] > 1:
        data = data.mean(axis=1)
    if sr != fs:
        data = resampy.resample(data, sr, fs)
        sr = fs

    # Extract melody using librosa's pyin
    print("Extracting melody f0 with librosa...")
    f0, voiced_flag, voiced_probs = librosa.pyin(data, fmin=librosa.note_to_hz('C2'),
                                                 fmax=librosa.note_to_hz('C7'),
                                                 sr=sr, hop_length=hop)

    # Fill in unvoiced sections with zeros
    pitch = np.nan_to_num(f0)

    print("Converting Hz to MIDI notes...")
    midi_pitch = hz2midi(pitch)

    print("Segmenting into MIDI notes...")
    notes = midi_to_notes(midi_pitch, fs, hop, smooth, minduration)

    print("Saving MIDI to disk...")
    save_midi(outfile, notes, bpm)

    if savejams:
        print("Saving JAMS to disk...")
        jamsfile = os.path.splitext(outfile)[0] + ".jams"
        track_duration = len(data) / float(fs)
        save_jams(jamsfile, notes, track_duration, os.path.basename(infile))

    print("Conversion complete.")

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("infile", help="Path to input audio file.")
    parser.add_argument("outfile", help="Path for saving output MIDI file.")
    parser.add_argument("bpm", type=int, help="Tempo of the track in BPM.")
    parser.add_argument("--smooth", type=float, default=0.25,
                        help="Smooth the pitch sequence with a median filter "
                             "of the provided duration (in seconds).")
    parser.add_argument("--minduration", type=float, default=0.1,
                        help="Minimum allowed duration for note (in seconds). "
                             "Shorter notes will be removed.")
    parser.add_argument("--jams", action="store_const", const=True,
                        default=False, help="Also save output in JAMS format.")

    args = parser.parse_args()
    audio_to_midi_melodia(args.infile, args.outfile, args.bpm, smooth=args.smooth, minduration=args.minduration, savejams=args.jams)

Thanks for sharing this and your other projects over the years, @justinsalamon. Great to see you delivering cool new stuff at MAX Sneaks this week!

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

No branches or pull requests

9 participants