150 lines
4.3 KiB
Python
150 lines
4.3 KiB
Python
##| Copyright: (C) 2019-2020 Kevin Larke <contact AT larke DOT org>
|
|
##| 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])
|
|
|
|
|
|
|
|
|