# genmidi.py is a JYTHON script, must be run via Jython # because it uses Java's MIDI library. D. Parson, spring 2020, # for csc558 assignment 3. Updated fall2020 and fall 2022 to # use a novel combination of notes, scales, tonics, keys. # The Fall 2022 version does NOT generate ARFF with more than # 1 note per row of data because that is part of the CSC523 assignment. # Updated again fall 2024 to accommodate cluster-based analysis. # NOTES: # 1. On MIDI file types: # http://midi-tutor.proboards.com/thread/44/9-midi-file-formats # "A type 0 MIDI file has all of the channel data on one track." # That is what I will use. I will write one file per channel so I can # drop them into Ableton Live. I will also write 1 ARFF file. # "A type 1 file splits the channel data to tracks. This is for the # convenience of being able to see and manipulate the data for one 'track', # or one instrument, easily within a software sequencing program." # "A type 2 MIDI file is sort of a combination of a type 0 and a type 1. # It contains multiple tracks, but each track contains a different and # complete sequence." We don't need type 1 or 2, although since I am # targeting Live (in addition to ARFF data), I will create one type 0 # MIDI file for each channel. # See also http://midi.teragonaudio.com/tech/midifile.htm # Tick/tempo https://docs.oracle.com/javase/tutorial/sound/MIDI-seq-intro.html from javax.sound import midi from java.io import File import random # https://docs.python.org/2/library/random.html import math # https://docs.python.org/2/library/math.html import datetime import sys def __genGaussianClosure__(sigma): ''' Bind sigma for random.gauss. The returned function takes a list, placing the Gaussian peak "mu" at element 0 and tailing out from there, mapping the tails to elements 1..n-1. "mu" = 0 is the mean, sigma is standard dev. ''' def genGaussian(modeNoteList): g = int(round(abs(random.gauss(0.0,sigma)))) % len(modeNoteList) return(modeNoteList[g]) return genGaussian def __genUniformClosure__(): ''' Bind random.uniform. The returned function takes a list, returning a value in a uniform distribution [0,n) for the argument list of length n. ''' def genUniform(modeNoteList): g = int(round(abs(random.uniform( 0,len(modeNoteList))))) % len(modeNoteList) return(modeNoteList[g]) return genUniform # A subset following musical *modes* (scales) will be used in the piece. # A mode becomes a nominal target attribute, identified uniquely # by its 12-note bitmap (1 for note on and 0 for note off), normalized # to a tonic of C. 0 in the note lists below is the interval for the # tonic, and 12 for the octave. # See https://en.wikipedia.org/wiki/Mode_(music) # The ORDER OF INTERVALS USED IN THESE LISTS ARE: # [tonic, octave, 5th, 3rd, 7th, 4th, 6th, 2nd] def reorderIntervals(modelist): return [modelist[0], modelist[7], modelist[4], modelist[2], modelist[6], modelist[3], modelist[5], modelist[1]] # Major modes (major 3rd) __IonianMode__ = [0, 2, 4, 5, 7, 9, 11, 12] # a.k.a. major scale __LydianMode__ = [0, 2, 4, 6, 7, 9, 11, 12] # fourth is sharp __MixolydianMode__ = [0, 2, 4, 5, 7, 9, 10, 12] # seventh is flat # Minor modes (minor 3rd) __AeolianMode__ = [0, 2, 3, 5, 7, 8, 10, 12] # natural minor scale __DorianMode__ = [0, 2, 3, 5, 7, 9, 10, 12] # sixth is sharp __Phrygian__ = [0, 1, 3, 5, 7, 8, 10, 12] # 2nd is flat # Locrian has Minor third, also known as a dimished mode because of flat 5th __LocrianMode__ = [0, 1, 3, 5, 6, 8, 10, 12] # 2nd is flat, 5th is flat # Chromatic is not a mode, it is just all the notes. __Chromatic__ = [i for i in range(0, 13)] __movementModes__ = [ # 4 entries per movement, 1 for each of 4 MIDI channels [__IonianMode__, __IonianMode__, __MixolydianMode__, __LydianMode__], [__AeolianMode__, __AeolianMode__, __DorianMode__, __Phrygian__], # Give the lead instrument Ionia in the dissonant section for added tension. [__IonianMode__, __LocrianMode__, __Chromatic__, __Chromatic__], [__IonianMode__, __IonianMode__, __MixolydianMode__, __LydianMode__], ] # reprise first movement in fourth movement __movementModeNames__ = [ ["Ionian", "Ionian", "Mixolydian", "Lydian"], ["Aeolian", "Aeolian", "Dorian", "Phrygian"], # Give the lead instrument Ionia in the dissonant section for added tension. ["Ionian", "Locrian", "Chromatic", "Chromatic"], ["Ionian", "Ionian", "Mixolydian", "Lydian"], ] # reprise first movement in fourth movement __AccentPatterns__ = [ # weights X 63, no random sampling [[2, 0, 2, 0, 1, 0, 2, 0], [1, 1, 0, 1, 1, 0, 1, 0], [1, 0, 0, 0, 1, 0, 0, 0], [2, 1, 0, 2, 1, 0, 2, 0]], [[1, 0, 1, 0, 1, 0, 1, 0], [0, 1, 0, 1, 0, 1, 0, 1], [1, 0, 0, 0, 1, 0, 0, 0], [1, 0, 1, 0, 1, 0, 1, 0]], [[1, 0, 1, 0, 1, 0, 1, 0], [0, 1, 0, 1, 0, 1, 0, 1], [1, 0, 1, 0, 1, 0, 1, 0], [0, 1, 0, 1, 0, 1, 0, 1]], # 8/8, 7/8, also 9/7 [[2, 2, 0, 2, 2, 0, 1, 0], [1, 0, 1, 1, 0, 1, 0], [1, 1, 0, 0, 1, 1, 0, 0], [2, 2, 0, 2, 2, 0, 1, 0, 1]] ] __RandomGenerators__ = [ # each instrument channel uses the same gen # There are 4 for 4 movements. __genGaussianClosure__(3), __genGaussianClosure__(3), __genUniformClosure__(), __genGaussianClosure__(4)] __TONIC__ = [7, 9, 7, 7] # Gmaj, Amin, G?, Gmaj by movement. __OCTAVE__ = [ # Range per channel, use uniform for this: [4, 5], [3, 4], [3, 4], [3, 5] ] __SUSTAIN__ = [2, 2, 4, 2] # per channel def writeARFFattributes(fhnd, timestring): ''' Write ARFF attribute lines at the start of the open file fhnd, where timestring is the generation timestamp with filename- compatible chars. ''' fhnd.write("@relation 'genConcert" + timestring + "'\n") fhnd.write("@attribute movement numeric\n") fhnd.write("@attribute channel numeric\n") fhnd.write("@attribute command {noteon, noteoff}\n") fhnd.write("@attribute notenum numeric\n") fhnd.write("@attribute velocity numeric\n") fhnd.write("@attribute tick numeric\n") # timestamp fhnd.write("@attribute ttonic numeric\n") # tagged tonic fhnd.write("@attribute tmode {Ionian,Mixolydian,Lydian,Aeolian,Dorian,Phrygian,Locrian,Chromatic}\n") # tagged musical scale fhnd.write("@data\n") def writeARFFtrack(fhnd, track, chan, movement, tonic, modename): for evtix in range(0, track.size()): evt = track.get(evtix) msg = evt.getMessage() # a ShortMessage if (isinstance(msg, midi.ShortMessage)): # tracks insert other things (end-of-track) on their own. tick = evt.getTick() cmd = msg.getCommand() c = msg.getChannel() data1 = msg.getData1(); data2 = msg.getData2() fhnd.write(str(movement)+"," + str(chan) + ",") if (cmd == midi.ShortMessage.NOTE_ON): fhnd.write("noteon,") elif (cmd == midi.ShortMessage.NOTE_OFF): fhnd.write("noteoff,") else: oops = ("ERROR: Invalid writeARFFtrack MIDI command on channel " + str(chan) + ", movement " + str(movement) + ", ticks = " + str(tick) + ": " + str(cmd)) sys.stderr.write(oops + '\n') raise ValueError(oops) if (c != chan): oops = ("ERROR: Channel passed to writeARFFtrack: " + str(chan) + " does not match channel in track " ", movement " + str(movement) + ", ticks = " + str(tick) + ", channel: " + str(c)) sys.stderr.write(oops + '\n') raise ValueError(oops) fhnd.write(str(data1) + "," + str(data2) + "," + str(tick) + ",") fhnd.write(str(tonic) + ",'" + modename + "'\n") def genConcert(): ''' genConcert() generates backing tracks for a March 24, 2020 performance by D. Parson on http://radio.electro-music.com, and it also generates Weka ARFF files for csc558 assignment 3. The performance will last 30 minutes, divided into 4 movements of 7.5 minutes each, with MAJOR SCALES, GAUSSIAN DISTRIBUTION FROM THE TONIC MINOR SCALES, GAUSSIAN DISTRIBUTION FROM THE TONIC UNIFORM RANDOM DISTRIBUTION OF NOTES FROM CHROMATIC MAJOR SCALES, GAUSSIAN DISTRIBUTION FROM THE TONIC 4 movements X 4 MIDI channels = 16 Type 0 (single-track) MIDI files. genConcert just shovels it all into 1 ARFF file with a movement tag, in case we decide to focus on specific mode types. Other tagged ARFF attributes include ''' timestring = str(datetime.datetime.now()).replace(' ','_').replace( '-','_').replace(':','_').replace('.','_') for arfftype, seed in [('train', 42), ('test', 12345)]: random.seed(seed) filename = "./MIDIdata/fall2022concert_" + arfftype + ".arff" midiprefix = "fall2022concert_" ; arfffile = open(filename,"w") writeARFFattributes(arfffile, timestring + '_' + arfftype) # From javax.sound.midi documentation: # "For tempo-based timing, divisionType is PPQ (pulses per quarter note) # and the resolution is specified in ticks per beat." divisionType = midi.Sequence.PPQ resolution = 1 # design decision, ability to split 1/4 note in pieces # play-time tempo is set by the sequencer numTracks = 1 notesPerMovement = 512 # After that just loop them during performance. curticks = [0 for chan in range(0, len(__SUSTAIN__))] for movement in range(0, len(__AccentPatterns__)): # sync the 4 channels at the start of each movement # ct = [max(curticks) for c in range(0, len(curticks))] ct = [0 for c in range(0, len(curticks))] # restart time per movement curticks = ct seq = [None for chan in range(0, len(__SUSTAIN__))] trk = [None for chan in range(0, len(__SUSTAIN__))] rndgen = __RandomGenerators__[movement] # all channels use the same for chan in range(0, len(__SUSTAIN__)): modename = __movementModeNames__[movement][chan] seq[chan] = midi.Sequence(divisionType, resolution, numTracks) trk[chan] = seq[chan].getTracks()[0] # Created with 1 track. mymode = reorderIntervals(__movementModes__[movement][chan]) tonic = __TONIC__[movement] sustain = __SUSTAIN__[chan] octaverange = __OCTAVE__[chan] accents = __AccentPatterns__[movement][chan] accentStep = 0 for noteix in range(0,notesPerMovement): # The Midi Sequence object sorts them on time. acc = accents[accentStep] accentStep = (accentStep+1) % len(accents) mytime = resolution * sustain # Needed even for a rest. if acc: # it may be a rest, then don't generate anything octave = octaverange[ random.randint(0,len(octaverange)-1)]*12 interval = rndgen(mymode) newnote = tonic + interval + octave velocity = (acc * 63) if ((acc * 63) < 128) else 127 noteMessage = midi.ShortMessage() noteMessage.setMessage(midi.ShortMessage.NOTE_ON, chan, newnote, velocity) evt = midi.MidiEvent(noteMessage, curticks[chan]) trk[chan].add(evt) noteMessage = midi.ShortMessage() noteMessage.setMessage(midi.ShortMessage.NOTE_OFF, chan, newnote, 0) evt = midi.MidiEvent(noteMessage, curticks[chan]+mytime) trk[chan].add(evt) curticks[chan] += mytime # notes done for this chan in this movement writeARFFtrack(arfffile, trk[chan], chan, movement, tonic, modename) midifname = ('./MIDIdata/' + midiprefix + "m" + str(movement) + "_c" + str(chan) + '_' + arfftype + ".mid") file = File(midifname) midi.MidiSystem.write(seq[chan], 0, file) # done with this chan # done with this movement arfffile.close() # done with train and test datasets if __name__ == '__main__': genConcert()