2020-10-26 18:57:43 +00:00
##| Copyright: (C) 2019-2020 Kevin Larke <contact AT larke DOT org>
##| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file.
2019-09-01 14:54:09 +00:00
import sys , os , argparse , types , logging , select , time , json
2019-08-20 01:18:57 +00:00
from datetime import datetime
import multiprocessing
from multiprocessing import Process , Pipe
from picadae_api import Picadae
from AudioDevice import AudioDevice
2019-11-25 01:07:47 +00:00
from MidiDevice import MidiDevice
2019-08-20 01:18:57 +00:00
from result import Result
2019-09-01 14:54:09 +00:00
from common import parse_yaml_cfg
from plot_seq import form_resample_pulse_time_list
2020-02-29 05:01:58 +00:00
from plot_seq_1 import get_resample_points_wrap
2019-11-09 16:13:34 +00:00
from plot_seq import form_final_pulse_list
2019-11-18 16:44:47 +00:00
from rt_note_analysis import RT_Analyzer
from keyboard import Keyboard
2019-11-25 01:07:47 +00:00
from calibrate import Calibrate
2020-02-29 05:01:58 +00:00
from rms_analysis import rms_analyze_one_rt_note_wrap
from MidiFilePlayer import MidiFilePlayer
2019-08-20 01:18:57 +00:00
class AttackPulseSeq :
2019-12-09 22:37:24 +00:00
""" Sequence a fixed pitch over a list of attack pulse lengths. """
2019-08-20 01:18:57 +00:00
2019-11-18 16:44:47 +00:00
def __init__ ( self , cfg , audio , api , noteDurMs = 1000 , pauseDurMs = 1000 ) :
self . cfg = cfg
2019-08-20 01:18:57 +00:00
self . audio = audio
self . api = api
self . outDir = None # directory to write audio file and results
2019-12-09 22:37:24 +00:00
self . pitch = None # pitch to paly
2019-08-20 01:18:57 +00:00
self . pulseUsL = [ ] # one onset pulse length in microseconds per sequence element
self . noteDurMs = noteDurMs # duration of each chord in milliseconds
self . pauseDurMs = pauseDurMs # duration between end of previous note and start of next
2019-11-18 16:44:47 +00:00
self . holdDutyPctL = None # hold voltage duty cycle table [ (minPulseSeqUsec,dutyCyclePct) ]
2020-02-29 05:01:58 +00:00
self . holdDutyPctD = None # { us:dutyPct } for each us in self.pulseUsL
self . silentNoteN = None
2019-08-20 01:18:57 +00:00
self . pulse_idx = 0 # Index of next pulse
self . state = None # 'note_on','note_off'
2019-11-09 16:13:34 +00:00
self . prevHoldDutyPct = None
2019-08-20 01:18:57 +00:00
self . next_ms = 0 # Time of next event (note-on or note_off)
2019-11-09 16:13:34 +00:00
self . eventTimeL = [ ] # Onset/offset time of each note [ [onset_ms,offset_ms] ] (used to locate the note in the audio file)
2019-08-20 01:18:57 +00:00
self . beginMs = 0
2019-09-01 14:54:09 +00:00
self . playOnlyFl = False
2019-11-18 16:44:47 +00:00
self . rtAnalyzer = RT_Analyzer ( )
2019-08-20 01:18:57 +00:00
2020-02-29 05:01:58 +00:00
def start ( self , ms , outDir , pitch , pulseUsL , holdDutyPctL , holdDutyPctD , playOnlyFl = False ) :
2019-08-20 01:18:57 +00:00
self . outDir = outDir # directory to write audio file and results
2019-12-09 22:37:24 +00:00
self . pitch = pitch # note to play
2019-08-20 01:18:57 +00:00
self . pulseUsL = pulseUsL # one onset pulse length in microseconds per sequence element
2019-11-18 16:44:47 +00:00
self . holdDutyPctL = holdDutyPctL
2020-02-29 05:01:58 +00:00
self . holdDutyPctD = holdDutyPctD
self . silentNoteN = 0
2019-08-20 01:18:57 +00:00
self . pulse_idx = 0
self . state = ' note_on '
2019-11-09 16:13:34 +00:00
self . prevHoldDutyPct = None
2019-08-20 01:18:57 +00:00
self . next_ms = ms + 500 # wait for 500ms to play the first note (this will guarantee that there is some empty space in the audio file before the first note)
2019-09-01 14:54:09 +00:00
self . eventTimeL = [ [ 0 , 0 ] for _ in range ( len ( pulseUsL ) ) ] # initialize the event time
2019-08-20 01:18:57 +00:00
self . beginMs = ms
2019-09-01 14:54:09 +00:00
self . playOnlyFl = playOnlyFl
2019-11-18 16:44:47 +00:00
# kpl if not playOnlyFl:
self . audio . record_enable ( True ) # start recording audio
2019-08-20 01:18:57 +00:00
self . tick ( ms ) # play the first note
def stop ( self , ms ) :
self . _send_note_off ( ) # be sure that all notes are actually turn-off
2019-09-01 14:54:09 +00:00
2019-11-18 16:44:47 +00:00
# kpl if not self.playOnlyFl:
self . audio . record_enable ( False ) # stop recording audio
2019-09-01 14:54:09 +00:00
2019-08-20 01:18:57 +00:00
self . _disable ( ) # disable this sequencer
2019-09-01 14:54:09 +00:00
if not self . playOnlyFl :
self . _write ( ) # write the results
2019-08-20 01:18:57 +00:00
def is_enabled ( self ) :
return self . state is not None
def tick ( self , ms ) :
# if next event time has arrived
if self . is_enabled ( ) 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 ' :
2020-02-29 05:01:58 +00:00
self . _note_off ( ms )
self . _count_silent_notes ( )
2019-08-20 01:18:57 +00:00
self . pulse_idx + = 1
# if all notes have been played
2020-02-29 05:01:58 +00:00
if self . pulse_idx > = len ( self . pulseUsL ) : # or self.silentNoteN >= self.cfg.maxSilentNoteCount:
2019-08-20 01:18:57 +00:00
self . stop ( ms )
else :
assert ( 0 )
2020-02-29 05:01:58 +00:00
def _count_silent_notes ( self ) :
annBegMs = self . eventTimeL [ self . pulse_idx ] [ 0 ]
annEndMs = self . eventTimeL [ self . pulse_idx ] [ 1 ]
minDurMs = self . cfg . silentNoteMinDurMs
maxPulseUs = self . cfg . silentNoteMaxPulseUs
resD = rms_analyze_one_rt_note_wrap ( self . audio , annBegMs , annEndMs , self . pitch , self . pauseDurMs , self . cfg . analysisArgs [ ' rmsAnalysisArgs ' ] )
print ( " %4.1f db %4i ms %i " % ( resD [ ' hm ' ] [ ' db ' ] , resD [ ' hm ' ] [ ' durMs ' ] , self . pulse_idx ) )
if resD is not None and resD [ ' hm ' ] [ ' durMs ' ] < minDurMs and self . pulseUsL [ self . pulse_idx ] < maxPulseUs :
self . silentNoteN + = 1
print ( " SILENT " , self . silentNoteN )
else :
self . silentNoteN = 0
2019-11-09 16:13:34 +00:00
2020-02-29 05:01:58 +00:00
return self . silentNoteN
2019-11-09 16:13:34 +00:00
def _get_duty_cycle ( self , pulseUsec ) :
2020-02-29 05:01:58 +00:00
return self . holdDutyPctD [ pulseUsec ]
2019-11-09 16:13:34 +00:00
dutyPct = self . holdDutyPctL [ 0 ] [ 1 ]
for refUsec , refDuty in self . holdDutyPctL :
if pulseUsec < refUsec :
break
dutyPct = refDuty
return dutyPct
def _set_duty_cycle ( self , pitch , pulseUsec ) :
dutyPct = self . _get_duty_cycle ( pulseUsec )
if dutyPct != self . prevHoldDutyPct :
self . api . set_pwm_duty ( pitch , dutyPct )
print ( " Hold Duty: " , dutyPct )
self . prevHoldDutyPct = dutyPct
2019-08-20 01:18:57 +00:00
def _note_on ( self , ms ) :
2019-09-01 14:54:09 +00:00
self . eventTimeL [ self . pulse_idx ] [ 0 ] = self . audio . buffer_sample_ms ( ) . value
2019-08-20 01:18:57 +00:00
self . next_ms = ms + self . noteDurMs
self . state = ' note_off '
2019-12-09 22:37:24 +00:00
pulse_usec = int ( self . pulseUsL [ self . pulse_idx ] )
self . _set_duty_cycle ( self . pitch , pulse_usec )
self . api . note_on_us ( self . pitch , pulse_usec )
print ( " note-on: " , self . pitch , self . pulse_idx , pulse_usec )
2019-08-20 01:18:57 +00:00
def _note_off ( self , ms ) :
2019-09-01 14:54:09 +00:00
self . eventTimeL [ self . pulse_idx ] [ 1 ] = self . audio . buffer_sample_ms ( ) . value
2019-08-20 01:18:57 +00:00
self . next_ms = ms + self . pauseDurMs
self . state = ' note_on '
2019-11-18 16:44:47 +00:00
if self . playOnlyFl :
begTimeMs = self . eventTimeL [ self . pulse_idx ] [ 0 ]
endTimeMs = self . eventTimeL [ self . pulse_idx ] [ 1 ]
2019-12-09 22:37:24 +00:00
self . rtAnalyzer . analyze_note ( self . audio , self . pitch , begTimeMs , endTimeMs , self . cfg . analysisArgs [ ' rmsAnalysisArgs ' ] )
2019-08-20 01:18:57 +00:00
self . _send_note_off ( )
def _send_note_off ( self ) :
2019-12-09 22:37:24 +00:00
self . api . note_off ( self . pitch )
#print("note-off:",self.pitch,self.pulse_idx)
2019-08-20 01:18:57 +00:00
def _disable ( self ) :
self . state = None
def _write ( self ) :
d = {
" pulseUsL " : self . pulseUsL ,
2019-12-09 22:37:24 +00:00
" pitch " : self . pitch ,
2019-08-20 01:18:57 +00:00
" noteDurMs " : self . noteDurMs ,
" pauseDurMs " : self . pauseDurMs ,
2019-11-09 16:13:34 +00:00
" holdDutyPctL " : self . holdDutyPctL ,
2019-08-20 01:18:57 +00:00
" eventTimeL " : self . eventTimeL ,
" beginMs " : self . beginMs
}
print ( " Writing: " , self . outDir )
outDir = os . path . expanduser ( self . outDir )
if not os . path . isdir ( outDir ) :
os . mkdir ( outDir )
with open ( os . path . join ( outDir , " seq.json " ) , " w " ) as f :
f . write ( json . dumps ( d ) )
self . audio . write_buffer ( os . path . join ( outDir , " audio.wav " ) )
class CalibrateKeys :
def __init__ ( self , cfg , audioDev , api ) :
self . cfg = cfg
2019-11-18 16:44:47 +00:00
self . seq = AttackPulseSeq ( cfg , audioDev , api , noteDurMs = cfg . noteDurMs , pauseDurMs = cfg . pauseDurMs )
2019-08-20 01:18:57 +00:00
self . pulseUsL = None
2019-12-09 22:37:24 +00:00
self . pitchL = None
2019-08-20 01:18:57 +00:00
self . pitch_idx = - 1
2019-12-09 22:37:24 +00:00
def start ( self , ms , pitchL , pulseUsL , playOnlyFl = False ) :
if len ( pitchL ) > 0 :
2019-08-20 01:18:57 +00:00
self . pulseUsL = pulseUsL
2019-12-09 22:37:24 +00:00
self . pitchL = pitchL
2019-08-20 01:18:57 +00:00
self . pitch_idx = - 1
2019-12-09 22:37:24 +00:00
self . _start_next_note ( ms , playOnlyFl )
2019-08-20 01:18:57 +00:00
def stop ( self , ms ) :
self . pitch_idx = - 1
self . seq . stop ( ms )
def is_enabled ( self ) :
return self . pitch_idx > = 0
def tick ( self , ms ) :
if self . is_enabled ( ) :
self . seq . tick ( ms )
# if the sequencer is done playing
if not self . seq . is_enabled ( ) :
2019-12-09 22:37:24 +00:00
self . _start_next_note ( ms , self . seq . playOnlyFl ) # ... else start the next sequence
2019-08-20 01:18:57 +00:00
return None
2019-12-09 22:37:24 +00:00
def _start_next_note ( self , ms , playOnlyFl ) :
2019-09-01 14:54:09 +00:00
2019-08-20 01:18:57 +00:00
self . pitch_idx + = 1
2019-12-09 22:37:24 +00:00
# if the last note in pitchL has been played ...
if self . pitch_idx > = len ( self . pitchL ) :
2019-08-20 01:18:57 +00:00
self . stop ( ms ) # ... then we are done
else :
2019-12-09 22:37:24 +00:00
pitch = self . pitchL [ self . pitch_idx ]
2019-08-20 01:18:57 +00:00
# be sure that the base directory exists
2019-12-09 22:37:24 +00:00
baseDir = os . path . expanduser ( cfg . outDir )
if not os . path . isdir ( baseDir ) :
os . mkdir ( baseDir )
2019-08-20 01:18:57 +00:00
2019-12-09 22:37:24 +00:00
outDir = os . path . join ( baseDir , str ( pitch ) )
2019-08-20 01:18:57 +00:00
2019-09-01 14:54:09 +00:00
if not os . path . isdir ( outDir ) :
os . mkdir ( outDir )
# get the next available output directory id
outDir_id = self . _calc_next_out_dir_id ( outDir )
2019-11-09 16:13:34 +00:00
print ( outDir_id , outDir )
2019-09-01 14:54:09 +00:00
# if this is not the first time this note has been sampled then get the resample locations
2020-02-29 05:01:58 +00:00
if ( outDir_id == 0 ) or self . cfg . useFullPulseListFl :
2019-12-09 22:37:24 +00:00
self . pulseUsL = self . cfg . full_pulseL
else :
#self.pulseUsL,_,_ = form_resample_pulse_time_list( outDir, self.cfg.analysisArgs )
self . pulseUsL = get_resample_points_wrap ( baseDir , pitch , self . cfg . analysisArgs )
2019-09-01 14:54:09 +00:00
2019-12-09 22:37:24 +00:00
holdDutyPctL = self . cfg . calibrateArgs [ ' holdDutyPctD ' ] [ pitch ]
2019-11-18 16:44:47 +00:00
2019-09-01 14:54:09 +00:00
if playOnlyFl :
2019-12-09 22:37:24 +00:00
self . pulseUsL , _ , holdDutyPctL = form_final_pulse_list ( outDir , pitch , self . cfg . analysisArgs , take_id = None )
2019-09-01 14:54:09 +00:00
2019-11-09 16:13:34 +00:00
noteN = cfg . analysisArgs [ ' auditionNoteN ' ]
self . pulseUsL = [ self . pulseUsL [ int ( round ( i * 126.0 / ( noteN - 1 ) ) ) ] for i in range ( noteN ) ]
2019-09-01 14:54:09 +00:00
2019-11-09 16:13:34 +00:00
else :
outDir = os . path . join ( outDir , str ( outDir_id ) )
if not os . path . isdir ( outDir ) :
os . mkdir ( outDir )
2019-09-01 14:54:09 +00:00
2020-02-29 05:01:58 +00:00
#------------------------
j = 0
holdDutyPctD = { }
for us in self . pulseUsL :
if j + 1 < len ( holdDutyPctL ) and us > = holdDutyPctL [ j + 1 ] [ 0 ] :
j + = 1
holdDutyPctD [ us ] = holdDutyPctL [ j ] [ 1 ]
#------------------------
if self . cfg . reversePulseListFl :
self . pulseUsL = [ us for us in reversed ( self . pulseUsL ) ]
2019-08-20 01:18:57 +00:00
# start the sequencer
2020-02-29 05:01:58 +00:00
self . seq . start ( ms , outDir , pitch , self . pulseUsL , holdDutyPctL , holdDutyPctD , playOnlyFl )
2019-08-20 01:18:57 +00:00
2019-09-01 14:54:09 +00:00
def _calc_next_out_dir_id ( self , outDir ) :
id = 0
while os . path . isdir ( os . path . join ( outDir , " %i " % id ) ) :
id + = 1
return id
2019-08-20 01:18:57 +00:00
# This is the main application API it is running in a child process.
class App :
def __init__ ( self ) :
2020-02-29 05:01:58 +00:00
self . cfg = None
self . audioDev = None
self . api = None
self . cal_keys = None
self . keyboard = None
self . calibrate = None
self . midiFilePlayer = None
2019-08-20 01:18:57 +00:00
def setup ( self , cfg ) :
self . cfg = cfg
self . audioDev = AudioDevice ( )
2019-11-25 01:07:47 +00:00
self . midiDev = MidiDevice ( )
2019-08-20 01:18:57 +00:00
2020-02-29 05:01:58 +00:00
res = None
2019-08-20 01:18:57 +00:00
#
# TODO: unify the result error handling
# (the API and the audio device return two diferent 'Result' types
#
2020-02-29 05:01:58 +00:00
if hasattr ( cfg , ' audio ' ) :
res = self . audioDev . setup ( * * cfg . audio )
2019-08-20 01:18:57 +00:00
2020-02-29 05:01:58 +00:00
if not res :
self . audio_dev_list ( 0 )
2019-09-01 14:54:09 +00:00
else :
2020-02-29 05:01:58 +00:00
self . audioDev = None
2019-08-20 01:18:57 +00:00
2020-02-29 05:01:58 +00:00
if True :
2019-11-25 01:07:47 +00:00
if hasattr ( cfg , ' midi ' ) :
res = self . midiDev . setup ( * * cfg . midi )
2019-08-20 01:18:57 +00:00
2019-11-25 01:07:47 +00:00
if not res :
self . midi_dev_list ( 0 )
2019-08-20 01:18:57 +00:00
else :
2019-11-25 01:07:47 +00:00
self . midiDev = None
2020-02-29 05:01:58 +00:00
self . api = Picadae ( key_mapL = cfg . key_mapL )
2019-11-25 01:07:47 +00:00
2020-02-29 05:01:58 +00:00
# wait for the letter 'a' to come back from the serial port
api_res = self . api . wait_for_serial_sync ( timeoutMs = cfg . serial_sync_timeout_ms )
2019-11-25 01:07:47 +00:00
2020-02-29 05:01:58 +00:00
# did the serial port sync fail?
if not api_res :
res . set_error ( " Serial port sync failed. " )
else :
print ( " Serial port sync ' ed " )
self . cal_keys = CalibrateKeys ( cfg , self . audioDev , self . api )
2019-08-20 01:18:57 +00:00
2020-02-29 05:01:58 +00:00
self . keyboard = Keyboard ( cfg , self . audioDev , self . api )
2019-08-20 01:18:57 +00:00
2020-02-29 05:01:58 +00:00
self . calibrate = None #Calibrate( cfg.calibrateArgs, self.audioDev, self.midiDev, self.api )
2019-11-25 01:07:47 +00:00
2020-02-29 05:01:58 +00:00
self . midiFilePlayer = MidiFilePlayer ( cfg , self . api , self . midiDev , cfg . midiFileFn )
2019-08-20 01:18:57 +00:00
return res
def tick ( self , ms ) :
2020-02-29 05:01:58 +00:00
if self . audioDev is not None :
self . audioDev . tick ( ms )
2019-11-18 16:44:47 +00:00
2019-11-25 01:07:47 +00:00
if self . cal_keys :
self . cal_keys . tick ( ms )
2019-11-18 16:44:47 +00:00
if self . keyboard :
self . keyboard . tick ( ms )
2019-11-25 01:07:47 +00:00
if self . calibrate :
self . calibrate . tick ( ms )
2019-08-20 01:18:57 +00:00
2020-02-29 05:01:58 +00:00
if self . midiFilePlayer :
self . midiFilePlayer . tick ( ms )
2019-08-20 01:18:57 +00:00
def audio_dev_list ( self , ms ) :
portL = self . audioDev . get_port_list ( True )
for port in portL :
print ( " chs: %4i label: %s " % ( port [ ' chN ' ] , port [ ' label ' ] ) )
2019-11-25 01:07:47 +00:00
def midi_dev_list ( self , ms ) :
d = self . midiDev . get_port_list ( True )
for port in d [ ' listL ' ] :
print ( " IN: " , port )
d = self . midiDev . get_port_list ( False )
for port in d [ ' listL ' ] :
print ( " OUT: " , port )
2019-08-20 01:18:57 +00:00
def calibrate_keys_start ( self , ms , pitchRangeL ) :
2019-12-09 22:37:24 +00:00
pitchL = [ pitch for pitch in range ( pitchRangeL [ 0 ] , pitchRangeL [ 1 ] + 1 ) ]
self . cal_keys . start ( ms , pitchL , cfg . full_pulseL )
2019-09-01 14:54:09 +00:00
def play_keys_start ( self , ms , pitchRangeL ) :
chordL = [ [ pitch ] for pitch in range ( pitchRangeL [ 0 ] , pitchRangeL [ 1 ] + 1 ) ]
2019-11-25 01:07:47 +00:00
self . cal_keys . start ( ms , chordL , cfg . full_pulseL , playOnlyFl = True )
2019-08-20 01:18:57 +00:00
2019-11-18 16:44:47 +00:00
def keyboard_start_pulse_idx ( self , ms , argL ) :
pitchL = [ pitch for pitch in range ( argL [ 0 ] , argL [ 1 ] + 1 ) ]
self . keyboard . start ( ms , pitchL , argL [ 2 ] , None )
def keyboard_repeat_pulse_idx ( self , ms , argL ) :
self . keyboard . repeat ( ms , argL [ 0 ] , None )
def keyboard_start_target_db ( self , ms , argL ) :
pitchL = [ pitch for pitch in range ( argL [ 0 ] , argL [ 1 ] + 1 ) ]
self . keyboard . start ( ms , pitchL , None , argL [ 2 ] )
def keyboard_repeat_target_db ( self , ms , argL ) :
self . keyboard . repeat ( ms , None , argL [ 0 ] )
2019-11-25 01:07:47 +00:00
def calibrate_start ( self , ms , argL ) :
self . calibrate . start ( ms )
def calibrate_play ( self , ms , argL ) :
self . calibrate . play ( ms )
2019-08-20 01:18:57 +00:00
def calibrate_keys_stop ( self , ms ) :
2019-11-25 01:07:47 +00:00
self . cal_keys . stop ( ms )
2019-11-18 16:44:47 +00:00
self . keyboard . stop ( ms )
2019-11-25 01:07:47 +00:00
self . calibrate . stop ( ms )
2020-02-29 05:01:58 +00:00
def midi_file_player_start ( self , ms ) :
self . midiFilePlayer . start ( ms )
def midi_file_player_stop ( self , ms ) :
self . midiFilePlayer . stop ( ms )
def pedal_down ( self , ms ) :
print ( " pedal_down " )
self . midiDev . send_controller ( 64 , 100 )
def pedal_up ( self , ms ) :
print ( " pedal_up " ) ;
self . midiDev . send_controller ( 64 , 0 )
2019-11-18 16:44:47 +00:00
2019-08-20 01:18:57 +00:00
def quit ( self , ms ) :
if self . api :
self . api . close ( )
def _send_error ( pipe , res ) :
if res is None :
return
if res . msg :
pipe . send ( [ { " type " : " error " , ' value ' : res . msg } ] )
def _send_error_msg ( pipe , msg ) :
_send_error ( pipe , Result ( None , msg ) )
def _send_quit ( pipe ) :
pipe . send ( [ { ' type ' : ' quit ' } ] )
# This is the application engine async. process loop
def app_event_loop_func ( pipe , cfg ) :
multiprocessing . get_logger ( ) . info ( " App Proc Started. " )
# create the asynchronous application object
app = App ( )
res = app . setup ( cfg )
# if the app did not initialize successfully
if not res :
_send_error ( pipe , res )
_send_quit ( pipe )
return
dt0 = datetime . now ( )
ms = 0
while True :
# have any message arrived from the parent process?
if pipe . poll ( ) :
msg = None
try :
msg = pipe . recv ( )
except EOFError :
return
if not hasattr ( app , msg . type ) :
_send_error_msg ( pipe , " Unknown message type: ' %s ' . " % ( msg . type ) )
else :
# get the command handler function in 'app'
func = getattr ( app , msg . type )
ms = int ( round ( ( datetime . now ( ) - dt0 ) . total_seconds ( ) * 1000.0 ) )
2019-09-01 14:54:09 +00:00
2019-08-20 01:18:57 +00:00
# call the command handler
if msg . value :
res = func ( ms , msg . value )
else :
res = func ( ms )
# handle any errors returned from the commands
_send_error ( pipe , res )
# if a 'quit' msg was recived then break out of the loop
if msg . type == ' quit ' :
_send_quit ( pipe )
break
# give some time to the system
2019-11-25 01:07:47 +00:00
time . sleep ( 0.05 )
2019-08-20 01:18:57 +00:00
# calc the tick() time stamp
ms = int ( round ( ( datetime . now ( ) - dt0 ) . total_seconds ( ) * 1000.0 ) )
2019-09-01 14:54:09 +00:00
2019-08-20 01:18:57 +00:00
# tick the app
app . tick ( ms )
class AppProcess ( Process ) :
def __init__ ( self , cfg ) :
self . parent_end , child_end = Pipe ( )
super ( AppProcess , self ) . __init__ ( target = app_event_loop_func , name = " AppProcess " , args = ( child_end , cfg ) )
self . doneFl = False
def send ( self , d ) :
# This function is called by the parent process to send an arbitrary msg to the App process
self . parent_end . send ( types . SimpleNamespace ( * * d ) )
return None
def recv ( self ) :
# This function is called by the parent process to receive lists of child messages.
msgL = None
if not self . doneFl and self . parent_end . poll ( ) :
msgL = self . parent_end . recv ( )
for msg in msgL :
if msg [ ' type ' ] == ' quit ' :
self . doneFl = True
return msgL
def isdone ( self ) :
return self . doneFl
class Shell :
def __init__ ( self , cfg ) :
self . appProc = None
self . parseD = {
2019-11-18 16:44:47 +00:00
' q ' : { " func " : ' quit ' , " minN " : 0 , " maxN " : 0 , " help " : " quit " } ,
' ? ' : { " func " : " _help " , " minN " : 0 , " maxN " : 0 , " help " : " Print usage text. " } ,
' a ' : { " func " : " audio_dev_list " , " minN " : 0 , " maxN " : 0 , " help " : " List the audio devices. " } ,
2019-11-25 01:07:47 +00:00
' m ' : { " func " : " midi_dev_list " , " minN " : 0 , " maxN " : 0 , " help " : " List the MIDI devices. " } ,
2019-11-18 16:44:47 +00:00
' c ' : { " func " : " calibrate_keys_start " , " minN " : 1 , " maxN " : 2 , " help " : " Calibrate a range of keys. " } ,
2019-11-25 01:07:47 +00:00
' d ' : { " func " : " calibrate_start " , " minN " : 1 , " maxN " : 1 , " help " : " Calibrate based on fixed db levels. " } ,
' D ' : { " func " : " calibrate_play " , " minN " : 1 , " maxN " : 1 , " help " : " Play back last calibration. " } ,
2019-11-18 16:44:47 +00:00
' s ' : { " func " : " calibrate_keys_stop " , " minN " : 0 , " maxN " : 0 , " help " : " Stop key calibration " } ,
' p ' : { " func " : " play_keys_start " , " minN " : 1 , " maxN " : 2 , " help " : " Play current calibration " } ,
' k ' : { " func " : " keyboard_start_pulse_idx " , " minN " : 3 , " maxN " : 3 , " help " : " Play pulse index across keyboard " } ,
' r ' : { " func " : " keyboard_repeat_pulse_idx " , " minN " : 1 , " maxN " : 1 , " help " : " Repeat pulse index across keyboard with new pulse_idx " } ,
' K ' : { " func " : " keyboard_start_target_db " , " minN " : 3 , " maxN " : 3 , " help " : " Play db across keyboard " } ,
' R ' : { " func " : " keyboard_repeat_target_db " , " minN " : 1 , " maxN " : 1 , " help " : " Repeat db across keyboard with new pulse_idx " } ,
2020-02-29 05:01:58 +00:00
' F ' : { " func " : " midi_file_player_start " , " minN " : 0 , " maxN " : 0 , " help " : " Play the MIDI file. " } ,
' f ' : { " func " : " midi_file_player_stop " , " minN " : 0 , " maxN " : 0 , " help " : " Stop the MIDI file. " } ,
' P ' : { " func " : " pedal_down " , " minN " : 0 , " maxN " : 0 , " help " : " Pedal down. " } ,
' U ' : { " func " : " pedal_up " , " minN " : 0 , " maxN " : 0 , " help " : " Pedal up. " } ,
2019-08-20 01:18:57 +00:00
}
def _help ( self , _ = None ) :
for k , d in self . parseD . items ( ) :
s = " {} = {} " . format ( k , d [ ' help ' ] )
print ( s )
return None
def _syntaxError ( self , msg ) :
return Result ( None , " Syntax Error: " + msg )
def _exec_cmd ( self , tokL ) :
if len ( tokL ) < = 0 :
return None
opcode = tokL [ 0 ]
if opcode not in self . parseD :
return self . _syntaxError ( " Unknown opcode: ' {} ' . " . format ( opcode ) )
d = self . parseD [ opcode ]
func_name = d [ ' func ' ]
func = None
# find the function associated with this command
if hasattr ( self , func_name ) :
func = getattr ( self , func_name )
try :
# convert the parameter list into integers
argL = [ int ( tokL [ i ] ) for i in range ( 1 , len ( tokL ) ) ]
except :
return self . _syntaxError ( " Unable to create integer arguments. " )
# validate the count of command args
if d [ ' minN ' ] != - 1 and ( d [ ' minN ' ] > len ( argL ) or len ( argL ) > d [ ' maxN ' ] ) :
return self . _syntaxError ( " Argument count mismatch. {} is out of range: {} to {} " . format ( len ( argL ) , d [ ' minN ' ] , d [ ' maxN ' ] ) )
# call the command function
if func :
result = func ( * argL )
else :
result = self . appProc . send ( { ' type ' : func_name , ' value ' : argL } )
return result
def run ( self ) :
# create the API object
self . appProc = AppProcess ( cfg )
self . appProc . start ( )
print ( " ' q ' =quit ' ? ' =help " )
time_out_secs = 1
# this is the shell main loop
while True :
# wait for keyboard activity
i , o , e = select . select ( [ sys . stdin ] , [ ] , [ ] , time_out_secs )
if i :
# read the command
s = sys . stdin . readline ( ) . strip ( )
# tokenize the command
tokL = s . split ( ' ' )
# execute the command
result = self . _exec_cmd ( tokL )
# if this is the 'quit' command
if tokL [ 0 ] == ' q ' :
break
# check for msg's from the async application process
if self . _handle_app_msgs ( self . appProc . recv ( ) ) :
break
# wait for the appProc to complete
while not self . appProc . isdone ( ) :
self . appProc . recv ( ) # drain the AppProc() as it shutdown
time . sleep ( 0.1 )
2019-09-01 14:54:09 +00:00
2019-08-20 01:18:57 +00:00
def _handle_app_msgs ( self , msgL ) :
quitAppFl = False
if msgL :
for msg in msgL :
if msg :
if msg [ ' type ' ] == ' error ' :
print ( " Error: {} " . format ( msg [ ' value ' ] ) )
elif msg [ ' type ' ] == ' quit ' :
quitAppFl = True
else :
print ( msg )
return quitAppFl
def parse_args ( ) :
""" Parse the command line arguments. """
descStr = """ Picadae auto-calibrate. """
logL = [ ' debug ' , ' info ' , ' warning ' , ' error ' , ' critical ' ]
ap = argparse . ArgumentParser ( description = descStr )
2019-11-09 16:13:34 +00:00
ap . add_argument ( " -c " , " --config " , default = " p_ac.yml " , help = " YAML configuration file. " )
2019-08-20 01:18:57 +00:00
ap . add_argument ( " -l " , " --log_level " , choices = logL , default = " warning " , help = " Set logging level: debug,info,warning,error,critical. Default:warning " )
return ap . parse_args ( )
if __name__ == " __main__ " :
logging . basicConfig ( )
#mplog = multiprocessing.log_to_stderr()
#mplog.setLevel(logging.INFO)
args = parse_args ( )
cfg = parse_yaml_cfg ( args . config )
shell = Shell ( cfg )
shell . run ( )