2.05 Stay in Instrument Range

In the Previous example the melody could get off track and get further away from the good sounding instrument range. It was also possible that it left the scale and created an error.

The Melody generation has new concept. The intervals are now generated step by step (intvl_next) with an for-loop. Each interval is first added to the melody and then checked for acceptance.

The range provides the acceptance values over the seven Midi-Octaves. At the moment a linear range is used, but it could be easily be changed in a Beta-Curve.

The acceptance function decides whether the proposed interval is accepted or if an new proposal must be made.

  1. first the acceptance values of the current and proposed note are read out of the range.
  2. then the acceptance value of the current note is divided by the acceptance value of the proposed note.
  3. This quotient is compared with an random number from 0 to 1. If Quotient > Random number, the proposed interval is accepted. Otherwise an new proposition is requested.

This is an Metropolis-Hasting Algorithm.

from pyknon.genmidi import Midi
from pyknon.music import Rest, Note, NoteSeq
import numpy as np
import matplotlib.pyplot as plt

Instruments: Available are at lest the 128 General-Midi (GM) Instruments. Depending on the sound-fonts there is a bigger choice. A list of the GM instruments can be found here. https://jazz-soft.net/demo/GeneralMidi.html

major = np.array([ 0, 2, 4, 5, 7, 9, 11])
minor = np.array([ 0, 2, 3, 5, 7, 8, 10])
blues = np.array([ 0, 3, 5, 6, 7, 10])
C7 = np.array([ 0, 4, 7, 10])
CM7 = np.array([ 0, 4, 7, 11])


def scale_create(tones):
    tones = np.asarray(tones)   # tones which form chord or scale in the first octave (0-11)
    if any(tones > 11):             # tones over one octave?
        tones = np.mod(tones,12)    # set the thones in one octave
        tones = np.sort(tones)      # sort the tones new
        tones = np.unique(tones)    # remove duplicate tones
    octave = np.repeat( np.linspace(0,108, num=10), len(tones))
    scale = np.add( octave, np.tile(tones, 10)) # add element wise octave and note
    return scale.astype(int)

def fade(start,end,steps):  # currently not in use
    fade = np.around( np.linspace(start,end,num=steps))
    fade = fade.astype(int)
    return fade

def ran_duration(duration, prob_duration, melody_len):
    duration= np.asarray(duration)                  # this are the allowed durations of the notes
    prob_duration = np.asarray(prob_duration)       # this are the probabilities how often each will occure
    prob_duration = prob_duration/np.sum(prob_duration)
    rythem = np.r_[np.random.choice(duration, size=melody_len, p=prob_duration)]
    return rythem

def ran_volume(volume, prob_volume, melody_len):
    volume = np.asarray(volume, dtype=int)         # this are the allowed volumes of thenotes
    prob_volume = np.asarray(prob_volume)          # this are the probabilities how often each volume will occure
    prob_volume = prob_volume/np.sum(prob_volume)
    volumes = np.r_[np.random.choice(volume, size=melody_len, p=prob_volume)]
    return volumes

def intvl_melody(intvl, prob_intvl, melody_len):  #Interval Melody  # currently not in use
    intvl = np.asarray(intvl)            # Possible interval
    prob_intvl = np.asarray(prob_intvl)         # Probability of each interval
    prob_intvl = prob_intvl/np.sum(prob_intvl)
    intervals = np.r_[np.random.choice(intvl, size=melody_len, p=prob_intvl)]
    imelody = np.cumsum(intervals)
    return imelody

linear_range: Generates the acceptance values. They define the range in which the instrument can play.

def liniar_range(r_start, r_top, r_edge, r_end, title): # acceptance range of the instrument
    h = 100 # hight of acceptance function
    a_range = np.zeros(121, dtype=int)  # only to midi =120 as 127 is not a complete octave
    np.put(a_range, range(r_start,r_top),  np.linspace(0,h, num=(r_top -r_start)) )
    np.put(a_range, range(r_top, r_edge),  np.linspace(h,h, num=(r_edge-r_top  )) )
    np.put(a_range, range(r_edge, r_end),  np.linspace(h,0, num=(r_end -r_edge )) )

    fig, ax = plt.subplots()
    ax.plot(range(121), a_range)
    plt.title(str(title)+':  '+str([r_start, r_top, r_edge, r_end]))
    plt.show()
    return a_range

i_last_note: finds the i value of the last note in the actual scale.

def i_last_note(note, scale):
    i_note = (np.abs(scale - note)).argmin()
    return i_note

intvl_next is a modification of intvl_melody. But it does only creates one interval and not an array/melody in one time.

def intvl_next(intvl, prob_intvl):  #singel interval
    intvl = np.asarray(intvl)            # Possible interval
    prob_intvl = np.asarray(prob_intvl)         # Probability of each interval
    prob_intvl = prob_intvl/np.sum(prob_intvl)
    interval = np.random.choice(intvl, size=1, p=prob_intvl)
    return interval[0]

acceptance decides with an Metropolis-Hasting Algorithm whether the Proposed not is accepted.

# x is the aceptance value of the current note, while x_new is it from the proposoal note.

def acceptance(x, x_new):
    if x_new < 1:
        if x < 1:
            print('start_note not in range')
            return
    quot = x_new/x
    if quot >= 1: return True
    if np.random.uniform(0,1)< quot: return True
    else: return False
def acceptance_melody(intvl, prob_intvl, scale, start_note, a_range, melody_len):
    melody = np.zeros(melody_len, dtype=int)
    melody[0] = scale[i_last_note(start_note,scale)]

    for npn in range(1, melody_len):
        accept = False
        while not accept:       # aslong acept == False
            inote = i_last_note(melody[npn-1],scale)
            inote_next = inote + intvl_next(intvl, prob_intvl)         # add current not with Proposition
            accept_val = a_range[[melody[(npn-1)],scale[inote_next]]]  # get acceptance values
            accept = acceptance(accept_val[0],accept_val[1])
        melody[npn] = scale[inote_next]
    print('melody:',melody)
    return melody

tune_P: Changing the scale creating method.

def tune_P():
    tune_name = 'tune_P'
    #np.random.seed(23)
    melody_len = 60
    scale = scale_create(blues)

    range_1 = liniar_range(48,56,72,78,'Range1')
    melody1 = acceptance_melody([-2,-1, 0, 1, 2],[1, 3, 1, 3, 1],scale, 60, range_1, melody_len)
    rythem1 = ran_duration([1/8, 1/4,1/2], [4,2,1], melody_len)
    volumes1 = ran_volume([0,120], [1,8], melody_len )

    notes1 = NoteSeq( [Note(no,octave=0, dur=du, volume=vo) for no,du,vo in zip(melody1,rythem1,volumes1)] )

    instruments = [24]
    notes = [notes1]
    return notes, instruments,tune_name

tune_P
tune_P


def gen_midi():
#     squezze into a MIDI framework
    notes, instruments, tune_name = tune_P() #  <--- select a tune  <<--     <<<<<<<<<--- select a tune -----
    nTracks = len(notes)

    m = Midi(number_tracks=nTracks, tempo=120, instrument=instruments)
    for iTrack in range(nTracks):
        m.seq_notes(notes[iTrack], track=iTrack)

    #--- write the MIDI file -----
    midi_file_name = tune_name +'.mid'   # set the name of the file
    m.write(midi_file_name)
    return midi_file_name

Midi: Play and Generate audio-file

External players offered a better sound quality in comparison with python libraries. We use VLC and Musescore. The soundfont for the VLC player is defined over the command line. For Musescore through the Gui in the preferences.

import subprocess
default_soundfont = '/usr/share/sounds/sf3/MuseScore_General.sf3'

def midi_play(midi_in, soundfont= default_soundfont):
    subprocess.call(['cvlc', midi_in , 'vlc://quit', '--soundfont', '/home/viturin/-vitis/Documents/MuseScore2/Soundfonts/Compifont_13082016.sf2'])   # cvlc = vlc without gui

def midi_audio(midi_in, name_out = 'none', soundfont= default_soundfont):
    if name_out == 'none' :
        name_out = midi_in.replace('.mid', '.flac')
    else:
        name_out = name_out + '.flac'
    subprocess.call(['mscore', '-o', name_out, midi_in]) # -o = export as

def midi_png(midi_in, name_out = 'none'):
    if name_out == 'none' :
        name_out = midi_in.replace('.mid', '.png')
    else:
        name_out = name_out + '.png'
    subprocess.call(['mscore', '-o', name_out, '-T', '2', midi_in]) # -o = export as , -T 2 = cut page with 2 pixel
######---  Main  ---######
midi_file_name = gen_midi()

midi_play(midi_file_name)
midi_audio(midi_file_name)
midi_png(midi_file_name)
../../_images/output_19_0.png
melody: [60 55 60 58 60 55 54 55 58 60 60 65 66 63 66 63 66 67 70 67 70 72 70 72
 67 72 72 70 72 70 72 72 67 67 67 67 67 72 70 75 72 70 70 72 70 72 70 67
 70 66 70 70 67 70 66 65 65 63 66 63]