##| Copyright: (C) 2019-2020 Kevin Larke ##| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file. import json class MidiFilePlayer: def __init__( self, cfg, api, midiDev, midiFn, velMapFn="velMapD.json" ): self.cfg = cfg self.api = api self.midiDev = midiDev self.midiL = [] # [ (us,status,d0,d1) ] self._parse_midi_file(midiFn) self.nextIdx = None self.startMs = 0 self.curDutyPctD = {} # { pitch:duty } track the current hold duty cycle of each note self.velMapD = {} self.holdDutyPctD = cfg.calibrateArgs['holdDutyPctD'] with open(velMapFn,'r') as f: velMapD = json.load(f) for pitch,usDbL in velMapD.items(): self.velMapD[ int(pitch) ] = usDbL def start(self, ms): self.nextIdx = 0 self.startMs = ms def stop( self, ms): self.nextIdx = None for pitch in self.velMapD.keys(): self.api.note_off( int(pitch) ) def tick( self, ms): if self.nextIdx is None: return curOffsMs = ms - self.startMs while self.nextIdx < len(self.midiL): if curOffsMs < self.midiL[ self.nextIdx ][0]: break cmd = self.midiL[ self.nextIdx ][1] if cmd == 'non': self._note_on(self.midiL[ self.nextIdx ][2],self.midiL[ self.nextIdx ][3]) elif cmd == 'nof': self._note_off(self.midiL[ self.nextIdx ][2]) elif cmd == 'ctl' and self.midiL[ self.nextIdx ][2] == 64: self.midiDev.send_controller(64,self.midiL[ self.nextIdx ][3]) self.nextIdx += 1 if self.nextIdx >= len(self.midiL): self.nextIdx = None def _get_duty_cycle( self, pitch, pulseUsec ): dutyPct = 50 if pitch in self.holdDutyPctD: dutyPct = self.holdDutyPctD[pitch][0][1] for refUsec,refDuty in self.holdDutyPctD[pitch]: print(pitch,refUsec,refDuty) if pulseUsec < refUsec: break dutyPct = refDuty return dutyPct def _set_duty_cycle( self, pitch, pulseUsec ): dutyPct = self._get_duty_cycle( pitch, pulseUsec ) if pitch not in self.curDutyPctD or self.curDutyPctD[pitch] != dutyPct: self.curDutyPctD[pitch] = dutyPct self.api.set_pwm_duty( pitch, dutyPct ) print("Hold Duty Set:",dutyPct) return dutyPct def _get_pulse_us( self, pitch, vel ): usDbL = self.velMapD[pitch] idx = round(vel * len(usDbL) / 127) if idx > len(usDbL): idx = len(usDbL)-1 us = usDbL[ idx ][0] print('non',pitch,vel,idx,us) return us def _note_on( self, pitch, vel ): if pitch not in self.velMapD: print("Missing pitch:",pitch) else: pulseUs = self._get_pulse_us(pitch,vel) self._set_duty_cycle( pitch, pulseUs ) self.api.note_on_us( pitch, pulseUs ) def _note_off( self, pitch ): self.api.note_off( pitch ) def _parse_midi_file( self,fn ): with open(fn,"r") as f: for lineNumb,line in enumerate(f): if lineNumb >= 3: tokenL = line.split() if len(tokenL) > 5: usec = int(tokenL[3]) status = None d0 = None d1 = None if tokenL[5] == 'non' or tokenL[5]=='nof' or tokenL[5]=='ctl': status = tokenL[5] d0 = int(tokenL[7]) d1 = int(tokenL[8]) self.midiL.append( (usec/1000,status,d0,d1)) self.midiL = sorted( self.midiL, key=lambda x: x[0] ) if __name__ == "__main__": midiFn = "/home/kevin/media/audio/midi/txt/988-v25.txt" mfp = MidiFilePlayer(None,None,midiFn) print(mfp.midiL[0:10])