##| Copyright: (C) 2019-2020 Kevin Larke ##| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file. import os,types,pickle import numpy as np from plot_seq import form_final_pulse_list from rms_analysis import rms_analysis_main_all class Keyboard: def __init__(self, cfg, audio, api): self.cfg = cfg self.audio = audio self.api = api self.keyD = {} # { midi_pitch: { pulseUsL, holdDutyPctL } } self.noteDurMs = cfg.noteDurMs self.pauseDurMs= cfg.pauseDurMs self.pitchL = None self.pitch_idx = None self.pulse_idx = None self.targetDb = None self.next_ms = None self.enableFl = False self.state = None #"note_on" | "note_off" self.rmsAnlD = None #self._load( cfg.outDir, cfg.analysisArgs) def load( self, inDir, pitchL, analysisArgsD ): self.keyD = {} inDir = os.path.expanduser(inDir) finalPulseListCacheFn = analysisArgsD['finalPulseListCacheFn'] if os.path.isfile(finalPulseListCacheFn): print("READING: final pulse list cache file: %s" % (finalPulseListCacheFn)) with open(finalPulseListCacheFn,'rb') as f: self.keyD = pickle.load(f) else: dirL = os.listdir(inDir) for dirStr in dirL: dirStr = os.path.normpath(os.path.join(inDir,dirStr)) if os.path.isdir(dirStr): pathL = dirStr.split(os.sep) midi_pitch = int( pathL[-1] ) if midi_pitch in pitchL: print(dirStr,midi_pitch) pulseUsL,pulseDbL,holdDutyPctL = form_final_pulse_list( dirStr, midi_pitch, analysisArgsD ) d = { 'pulseUsL':pulseUsL, 'holdDutyPctL':holdDutyPctL, 'lastDutyPct':0 } self.keyD[ midi_pitch ] = types.SimpleNamespace(**d) with open(finalPulseListCacheFn,'wb') as f: pickle.dump(self.keyD,f) print("Loading analysis ...") cacheFn = analysisArgsD['rmsAnalysisCacheFn'] self.rmsAnlD = rms_analysis_main_all( inDir, cacheFn, **analysisArgsD['rmsAnalysisArgs'] ) print("Load DONE.") def _get_duty_cycle_from_pulse_usec( self, pitch, pulseUsec ): if pitch not in self.keyD: print("Missing duty cycle.") return None dutyPct = self.keyD[pitch].holdDutyPctL[0][1] for refUsec,refDuty in self.keyD[pitch].holdDutyPctL: if pulseUsec < refUsec: break dutyPct = refDuty return dutyPct def _get_pulse_and_duty_cycle_from_pulse_idx( self, pitch, pulse_idx ): pulseUsec = self.keyD[ pitch ].pulseUsL[ pulse_idx ] dutyPct = self._get_duty_cycle_from_pulse_usec( pitch, pulseUsec ) return pulseUsec, dutyPct def _get_pulse_and_duty_cycle_target_db( self, pitch, targetDb ): r = self.rmsAnlD[pitch] pulse_idx = np.argmin( np.abs(np.array(r.pkDbL) - targetDb) ) print("PULSE idx:",pulse_idx," db:", r.pkDbL[pulse_idx] ) pulseUsec = r.pkUsL[pulse_idx] dutyPct = self._get_duty_cycle_from_pulse_usec( pitch, pulseUsec ) return pulseUsec, dutyPct def _get_pulse_and_duty_cycle( self, pitch, pulse_idx, targetDb ): if pulse_idx is not None: return self._get_pulse_and_duty_cycle_from_pulse_idx(pitch,pulse_idx) else: return self._get_pulse_and_duty_cycle_target_db( pitch, targetDb ) def start( self, ms, pitchL, pulse_idx, targetDb=None ): loadFl = True if self.pitchL is not None: loadFl = False for pitch in pitchL: if pitch not in self.pitchL: loadFl = True break if loadFl: self.load(self.cfg.outDir, pitchL, self.cfg.analysisArgs) self.pitchL = pitchL self.pitch_idx = 0 self.pulse_idx = pulse_idx self.targetDb = targetDb self.state = "note_on" self.next_ms = ms self.eventTimeL = [[0,0] for _ in range(len(pitchL))] # initialize the event time self.audio.record_enable(True) # start recording audio self.tick(ms) # play the first note def repeat( self, ms, pulse_idx, targetDb=None ): self.start( ms, self.pitchL, pulse_idx, targetDb ) def stop( self, ms ): self._send_note_off() self.audio.record_enable(False) # stop recording audio self.state = None # disable this sequencer def tick( self, ms ): #self.audio.tick(ms) # if next event time has arrived if self.state is not None and ms >= self.next_ms: # if waiting to turn note on if self.state == 'note_on': self._note_on(ms) # if waiting to turn a note off elif self.state == 'note_off': self._note_off(ms) self.pitch_idx += 1 # if all notes have been played if self.pitch_idx >= len(self.pitchL): self.stop(ms) else: assert(0) def _note_on( self, ms ): self.eventTimeL[ self.pitch_idx ][0] = self.audio.buffer_sample_ms().value self.next_ms = ms + self.noteDurMs self.state = 'note_off' pitch = self.pitchL[ self.pitch_idx ] pulse_usec, dutyPct = self._get_pulse_and_duty_cycle( pitch, self.pulse_idx, self.targetDb ) if pulse_usec is not None and dutyPct is not None: self._set_pwm_duty( pitch, dutyPct ) self.api.note_on_us( pitch, pulse_usec ) pulse_idx = 0 if self.pulse_idx is None else self.pulse_idx targetDb = 0 if self.targetDb is None else self.targetDb dutyPct = 0 if dutyPct is None else dutyPct print("note-on: %i %i %4.1f %8.1f %i" % (pitch, pulse_idx, targetDb, pulse_usec, dutyPct)) def _set_pwm_duty( self, pitch, dutyPct ): if self.keyD[pitch].lastDutyPct != dutyPct: self.keyD[pitch].lastDutyPct = dutyPct self.api.set_pwm_duty( pitch, dutyPct ) def _note_off( self, ms ): self.eventTimeL[ self.pitch_idx ][1] = self.audio.buffer_sample_ms().value self.next_ms = ms + self.pauseDurMs self.state = 'note_on' #begTimeMs = self.eventTimeL[ self.pulse_idx ][0] #endTimeMs = self.eventTimeL[ self.pulse_idx ][1] #self.rtAnalyzer.analyze_note( self.audio, self.pitchL[0], begTimeMs, endTimeMs, self.cfg.analysisArgs['rmsAnalysisArgs'] ) self._send_note_off() def _send_note_off( self ): for pitch in self.pitchL: self.api.note_off( pitch ) #print("note-off:",pitch,self.pulse_idx)