picadae calibration programs
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

p_ac.py 29KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854
  1. ##| Copyright: (C) 2019-2020 Kevin Larke <contact AT larke DOT org>
  2. ##| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file.
  3. import sys,os,argparse,types,logging,select,time,json
  4. from datetime import datetime
  5. import multiprocessing
  6. from multiprocessing import Process, Pipe
  7. from picadae_api import Picadae
  8. from AudioDevice import AudioDevice
  9. from MidiDevice import MidiDevice
  10. from result import Result
  11. from common import parse_yaml_cfg
  12. from plot_seq import form_resample_pulse_time_list
  13. from plot_seq_1 import get_resample_points_wrap
  14. from plot_seq import form_final_pulse_list
  15. from rt_note_analysis import RT_Analyzer
  16. from keyboard import Keyboard
  17. from calibrate import Calibrate
  18. from rms_analysis import rms_analyze_one_rt_note_wrap
  19. from MidiFilePlayer import MidiFilePlayer
  20. from VelTablePlayer import VelTablePlayer
  21. from NoteTester import NoteTester
  22. from PolyNoteTester import PolyNoteTester
  23. from ChordTester import ChordTester
  24. class AttackPulseSeq:
  25. """ Sequence a fixed pitch over a list of attack pulse lengths."""
  26. def __init__(self, cfg, audio, api, noteDurMs=1000, pauseDurMs=1000 ):
  27. self.cfg = cfg
  28. self.audio = audio
  29. self.api = api
  30. self.outDir = None # directory to write audio file and results
  31. self.pitch = None # pitch to paly
  32. self.pulseUsL = [] # one onset pulse length in microseconds per sequence element
  33. self.noteDurMs = noteDurMs # duration of each chord in milliseconds
  34. self.pauseDurMs = pauseDurMs # duration between end of previous note and start of next
  35. self.holdDutyPctL= None # hold voltage duty cycle table [ (minPulseSeqUsec,dutyCyclePct) ]
  36. self.holdDutyPctD= None # { us:dutyPct } for each us in self.pulseUsL
  37. self.silentNoteN = None
  38. self.pulse_idx = 0 # Index of next pulse
  39. self.state = None # 'note_on','note_off'
  40. self.prevHoldDutyPct = None
  41. self.next_ms = 0 # Time of next event (note-on or note_off)
  42. self.eventTimeL = [] # Onset/offset time of each note [ [onset_ms,offset_ms] ] (used to locate the note in the audio file)
  43. self.beginMs = 0
  44. self.playOnlyFl = False
  45. self.rtAnalyzer = RT_Analyzer()
  46. def start( self, ms, outDir, pitch, pulseUsL, holdDutyPctL, holdDutyPctD, playOnlyFl=False ):
  47. self.outDir = outDir # directory to write audio file and results
  48. self.pitch = pitch # note to play
  49. self.pulseUsL = pulseUsL # one onset pulse length in microseconds per sequence element
  50. self.holdDutyPctL = holdDutyPctL
  51. self.holdDutyPctD = holdDutyPctD
  52. self.silentNoteN = 0
  53. self.pulse_idx = 0
  54. self.state = 'note_on'
  55. self.prevHoldDutyPct = None
  56. 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)
  57. self.eventTimeL = [[0,0] for _ in range(len(pulseUsL))] # initialize the event time
  58. self.beginMs = ms
  59. self.playOnlyFl = playOnlyFl
  60. # kpl if not playOnlyFl:
  61. self.audio.record_enable(True) # start recording audio
  62. #print("Hold Delay from end of attack:",cfg.defaultHoldDelayUsecs)
  63. # self.api.set_hold_delay(pitch,cfg.defaultHoldDelayUsecs)
  64. self.api.set_hold_duty(pitch,35)
  65. print("Hold Duty Cycle=35.")
  66. self.tick(ms) # play the first note
  67. def stop(self, ms):
  68. self._send_note_off() # be sure that all notes are actually turn-off
  69. # kpl if not self.playOnlyFl:
  70. self.audio.record_enable(False) # stop recording audio
  71. self._disable() # disable this sequencer
  72. if not self.playOnlyFl:
  73. self._write() # write the results
  74. def is_enabled(self):
  75. return self.state is not None
  76. def tick(self, ms):
  77. # if next event time has arrived
  78. if self.is_enabled() and ms >= self.next_ms:
  79. # if waiting to turn note on
  80. if self.state == 'note_on':
  81. self._note_on(ms)
  82. # if waiting to turn a note off
  83. elif self.state == 'note_off':
  84. self._note_off(ms)
  85. self._count_silent_notes()
  86. self.pulse_idx += 1
  87. # if all notes have been played
  88. if self.pulse_idx >= len(self.pulseUsL): # or self.silentNoteN >= self.cfg.maxSilentNoteCount:
  89. self.stop(ms)
  90. else:
  91. assert(0)
  92. def _count_silent_notes( self ):
  93. annBegMs = self.eventTimeL[ self.pulse_idx ][0]
  94. annEndMs = self.eventTimeL[ self.pulse_idx ][1]
  95. minDurMs = self.cfg.silentNoteMinDurMs
  96. maxPulseUs = self.cfg.silentNoteMaxPulseUs
  97. resD = rms_analyze_one_rt_note_wrap( self.audio, annBegMs, annEndMs, self.pitch, self.pauseDurMs, self.cfg.analysisArgs['rmsAnalysisArgs'] )
  98. print( " %4.1f db %4i ms %i" % (resD['hm']['db'], resD['hm']['durMs'], self.pulse_idx))
  99. if resD is not None and resD['hm']['durMs'] < minDurMs and self.pulseUsL[ self.pulse_idx ] < maxPulseUs:
  100. self.silentNoteN += 1
  101. print("SILENT", self.silentNoteN)
  102. else:
  103. self.silentNoteN = 0
  104. return self.silentNoteN
  105. def _get_duty_cycle( self, pulseUsec ):
  106. return self.holdDutyPctD[ pulseUsec ]
  107. def _set_duty_cycle( self, pitch, pulseUsec ):
  108. if False:
  109. dutyPct = self._get_duty_cycle( pulseUsec )
  110. if dutyPct != self.prevHoldDutyPct:
  111. # self.api.set_pwm_duty( pitch, dutyPct )
  112. # print("Hold Duty:",dutyPct)
  113. pass
  114. self.prevHoldDutyPct = dutyPct
  115. if False:
  116. maxLevel = 64 # 225 # 64
  117. minLevel = 24 # 89 # 24
  118. maxUsec = 10000
  119. minUsec = 2500
  120. decayLevel = maxLevel
  121. if pulseUsec < maxUsec and pulseUsec >= minUsec:
  122. decayLevel = minLevel + ((pulseUsec-minUsec)/(maxUsec-minUsec)) * (maxLevel-minLevel)
  123. else:
  124. if pulseUsec < minUsec:
  125. decayLevel = minLevel
  126. decayLevel = int(decayLevel)
  127. decayLevel = self.api.calc_decay_level( pulseUsec )
  128. self.api.set_decay_level( pitch, decayLevel )
  129. print("Decay Level:", decayLevel)
  130. def _note_on( self, ms ):
  131. self.eventTimeL[ self.pulse_idx ][0] = self.audio.buffer_sample_ms().value
  132. self.next_ms = ms + self.noteDurMs
  133. self.state = 'note_off'
  134. pulse_usec = int(self.pulseUsL[ self.pulse_idx ])
  135. # decay_level = self.api.calc_decay_level( pulse_usec )
  136. #self._set_duty_cycle( self.pitch, pulse_usec )
  137. self.api.note_on_us( self.pitch, pulse_usec )
  138. print("note-on:",self.pitch, self.pulse_idx, pulse_usec, "dcy:", decay_level)
  139. def _note_off( self, ms ):
  140. self.eventTimeL[ self.pulse_idx ][1] = self.audio.buffer_sample_ms().value
  141. self.next_ms = ms + self.pauseDurMs
  142. self.state = 'note_on'
  143. if self.playOnlyFl:
  144. begTimeMs = self.eventTimeL[ self.pulse_idx ][0]
  145. endTimeMs = self.eventTimeL[ self.pulse_idx ][1]
  146. self.rtAnalyzer.analyze_note( self.audio, self.pitch, begTimeMs, endTimeMs, self.cfg.analysisArgs['rmsAnalysisArgs'] )
  147. self._send_note_off()
  148. def _send_note_off( self ):
  149. self.api.note_off( self.pitch )
  150. #print("note-off:",self.pitch,self.pulse_idx)
  151. def _disable(self):
  152. self.state = None
  153. def _write( self ):
  154. d = {
  155. "pulseUsL":self.pulseUsL,
  156. "pitch":self.pitch,
  157. "noteDurMs":self.noteDurMs,
  158. "pauseDurMs":self.pauseDurMs,
  159. "holdDutyPctL":self.holdDutyPctL,
  160. "eventTimeL":self.eventTimeL,
  161. "beginMs":self.beginMs
  162. }
  163. print("Writing: ", self.outDir )
  164. outDir = os.path.expanduser(self.outDir)
  165. if not os.path.isdir(outDir):
  166. os.mkdir(outDir)
  167. with open(os.path.join( outDir, "seq.json" ),"w") as f:
  168. f.write(json.dumps( d ))
  169. self.audio.write_buffer( os.path.join( outDir, "audio.wav" ) )
  170. class CalibrateKeys:
  171. def __init__(self, cfg, audioDev, api):
  172. self.cfg = cfg
  173. self.seq = AttackPulseSeq( cfg, audioDev, api, noteDurMs=cfg.noteDurMs, pauseDurMs=cfg.pauseDurMs )
  174. self.pulseUsL = None
  175. self.pitchL = None
  176. self.pitch_idx = -1
  177. def start( self, ms, pitchL, pulseUsL, playOnlyFl=False ):
  178. if len(pitchL) > 0:
  179. self.pulseUsL = pulseUsL
  180. self.pitchL = pitchL
  181. self.pitch_idx = -1
  182. self._start_next_seq( ms, playOnlyFl )
  183. def stop( self, ms ):
  184. self.pitch_idx = -1
  185. self.seq.stop(ms)
  186. def is_enabled( self ):
  187. return self.pitch_idx >= 0
  188. def tick( self, ms ):
  189. if self.is_enabled():
  190. self.seq.tick(ms)
  191. # if the sequencer is done playing
  192. if not self.seq.is_enabled():
  193. self._start_next_seq( ms, self.seq.playOnlyFl ) # ... else start the next sequence
  194. return None
  195. def _start_next_seq( self, ms, playOnlyFl ):
  196. self.pitch_idx += 1
  197. # if the last note in pitchL has been played ...
  198. if self.pitch_idx >= len(self.pitchL):
  199. self.stop(ms) # ... then we are done
  200. else:
  201. pitch = self.pitchL[ self.pitch_idx ]
  202. # be sure that the base directory exists
  203. baseDir = os.path.expanduser( cfg.outDir )
  204. if not os.path.isdir( baseDir ):
  205. os.mkdir( baseDir )
  206. outDir = os.path.join(baseDir, str(pitch) )
  207. if not os.path.isdir(outDir):
  208. os.mkdir(outDir)
  209. # get the next available output directory id
  210. outDir_id = self._calc_next_out_dir_id( outDir )
  211. # if this is not the first time this note has been sampled then get the resample locations
  212. if (outDir_id == 0) or self.cfg.useFullPulseListFl:
  213. self.pulseUsL = self.cfg.full_pulseL
  214. else:
  215. #self.pulseUsL,_,_ = form_resample_pulse_time_list( outDir, self.cfg.analysisArgs )
  216. self.pulseUsL = get_resample_points_wrap( baseDir, pitch, self.cfg.analysisArgs )
  217. holdDutyPctL = self.cfg.calibrateArgs['holdDutyPctD'][pitch]
  218. # if playback of the current calibration was requested
  219. if playOnlyFl:
  220. # form the calibrated pulse list
  221. self.pulseUsL,_,holdDutyPctL = form_final_pulse_list( baseDir, pitch, self.cfg.analysisArgs, take_id=None )
  222. noteN = cfg.analysisArgs['auditionNoteN']
  223. self.pulseUsL = [ self.pulseUsL[ int(round(i*126.0/(noteN-1)))] for i in range(noteN) ]
  224. else:
  225. outDir = os.path.join( outDir, str(outDir_id) )
  226. if not os.path.isdir(outDir):
  227. os.mkdir(outDir)
  228. #------------------------
  229. j = 0
  230. holdDutyPctD = {}
  231. for us in self.pulseUsL:
  232. if j+1<len(holdDutyPctL) and us >= holdDutyPctL[j+1][0]:
  233. j += 1
  234. holdDutyPctD[ us ] = holdDutyPctL[j][1]
  235. #------------------------
  236. if self.cfg.reversePulseListFl:
  237. self.pulseUsL = [ us for us in reversed(self.pulseUsL) ]
  238. # start the sequencer
  239. self.seq.start( ms, outDir, pitch, self.pulseUsL, holdDutyPctL, holdDutyPctD, playOnlyFl )
  240. def _calc_next_out_dir_id( self, outDir ):
  241. id = 0
  242. while os.path.isdir( os.path.join(outDir,"%i" % id)):
  243. id += 1
  244. return id
  245. # This is the main application API it is running in a child process.
  246. class App:
  247. def __init__(self ):
  248. self.cfg = None
  249. self.audioDev = None
  250. self.api = None
  251. self.cal_keys = None
  252. self.keyboard = None
  253. self.calibrate = None
  254. self.midiFilePlayer = None
  255. self.velTablePlayer = None
  256. self.noteTester = None
  257. self.polyNoteTester = None
  258. self.chordTester = None
  259. def setup( self, cfg ):
  260. self.cfg = cfg
  261. self.audioDev = AudioDevice()
  262. self.midiDev = MidiDevice()
  263. res = None
  264. #
  265. # TODO: unify the result error handling
  266. # (the API and the audio device return two diferent 'Result' types
  267. #
  268. if hasattr(cfg,'audio'):
  269. res = self.audioDev.setup(**cfg.audio)
  270. if not res:
  271. self.audio_dev_list(0)
  272. else:
  273. print("The cfg file has no audio stanza. No audio device will be initialized.")
  274. self.audioDev = None
  275. if True:
  276. if hasattr(cfg,'midi'):
  277. res = self.midiDev.setup(**cfg.midi)
  278. if not res:
  279. self.midi_dev_list(0)
  280. else:
  281. self.midiDev = None
  282. self.api = Picadae( key_mapL=cfg.key_mapL)
  283. # wait for the letter 'a' to come back from the serial port
  284. api_res = self.api.wait_for_serial_sync(timeoutMs=cfg.serial_sync_timeout_ms)
  285. # did the serial port sync fail?
  286. if not api_res:
  287. res.set_error("Serial port sync failed.")
  288. else:
  289. print("Serial port sync'ed")
  290. self.cal_keys = CalibrateKeys( cfg, self.audioDev, self.api )
  291. self.keyboard = Keyboard( cfg, self.audioDev, self.api )
  292. self.calibrate = None #Calibrate( cfg.calibrateArgs, self.audioDev, self.midiDev, self.api )
  293. self.midiFilePlayer = MidiFilePlayer( cfg, self.api, self.midiDev, cfg.midiFileFn )
  294. self.velTablePlayer = VelTablePlayer( cfg, self.api, self.audioDev, self.cfg.calibrateArgs['holdDutyPctD'], "velMapD.json")
  295. self.noteTester = NoteTester(cfg,self.api)
  296. self.polyNoteTester = PolyNoteTester(cfg,self.api)
  297. self.chordTester = ChordTester(cfg,self.api)
  298. return res
  299. def tick( self, ms ):
  300. if self.audioDev is not None:
  301. self.audioDev.tick(ms)
  302. if self.cal_keys:
  303. self.cal_keys.tick(ms)
  304. if self.keyboard:
  305. self.keyboard.tick(ms)
  306. if self.calibrate:
  307. self.calibrate.tick(ms)
  308. if self.midiFilePlayer:
  309. self.midiFilePlayer.tick(ms)
  310. if self.midiDev:
  311. msgL = self.midiDev.get_input()
  312. for msg in msgL:
  313. print(msg['value']);
  314. if self.velTablePlayer:
  315. self.velTablePlayer.tick(ms)
  316. if self.noteTester:
  317. self.noteTester.tick(ms)
  318. if self.polyNoteTester:
  319. self.polyNoteTester.tick(ms)
  320. if self.chordTester:
  321. self.chordTester.tick(ms)
  322. def audio_dev_list( self, ms ):
  323. if self.audioDev is not None:
  324. portL = self.audioDev.get_port_list( True )
  325. for port in portL:
  326. print("chs:%4i label:'%s'" % (port['chN'],port['label']))
  327. def midi_dev_list( self, ms ):
  328. d = self.midiDev.get_port_list( True )
  329. for port in d['listL']:
  330. print("IN:",port)
  331. d = self.midiDev.get_port_list( False )
  332. for port in d['listL']:
  333. print("OUT:",port)
  334. def calibrate_keys_start( self, ms, pitchRangeL ):
  335. pitchL = [ pitch for pitch in range(pitchRangeL[0], pitchRangeL[1]+1)]
  336. self.cal_keys.start( ms, pitchL, cfg.full_pulseL )
  337. def play_keys_start( self, ms, pitchRangeL ):
  338. chordL = [ pitch for pitch in range(pitchRangeL[0], pitchRangeL[1]+1)]
  339. self.cal_keys.start( ms, chordL, cfg.full_pulseL, playOnlyFl=True )
  340. def keyboard_start_pulse_idx( self, ms, argL ):
  341. pitchL = [ pitch for pitch in range(argL[0], argL[1]+1)]
  342. self.keyboard.start( ms, pitchL, argL[2], None )
  343. def keyboard_repeat_pulse_idx( self, ms, argL ):
  344. self.keyboard.repeat( ms, argL[0], None )
  345. def keyboard_start_target_db( self, ms, argL ):
  346. pitchL = [ pitch for pitch in range(argL[0], argL[1]+1)]
  347. self.keyboard.start( ms, pitchL, None, argL[2] )
  348. def keyboard_repeat_target_db( self, ms, argL ):
  349. self.keyboard.repeat( ms, None, argL[0] )
  350. def calibrate_start( self, ms, argL ):
  351. self.calibrate.start(ms)
  352. def calibrate_play( self, ms, argL ):
  353. self.calibrate.play(ms)
  354. def calibrate_keys_stop( self, ms ):
  355. self.cal_keys.stop(ms)
  356. self.keyboard.stop(ms)
  357. self.calibrate.stop(ms)
  358. def midi_file_player_start( self, ms ):
  359. self.midiFilePlayer.start(ms)
  360. def midi_file_player_stop( self, ms ):
  361. self.midiFilePlayer.stop(ms)
  362. def vel_table_updown( self, ms, argL):
  363. self.velTablePlayer.start(argL[0],argL[1],"updown")
  364. def vel_table_across( self, ms, argL ):
  365. self.velTablePlayer.start(argL[0],argL[1],"across")
  366. def vel_table_stop( self, ms ):
  367. self.velTablePlayer.stop()
  368. def note_tester_start( self, ms ):
  369. self.noteTester.start();
  370. def note_tester_stop( self, ms ):
  371. self.noteTester.stop();
  372. def poly_note_tester_start( self, ms ):
  373. self.polyNoteTester.start();
  374. def poly_note_tester_stop( self, ms ):
  375. self.polyNoteTester.stop();
  376. def chord_tester_start( self, ms ):
  377. self.chordTester.start()
  378. def chord_tester_stop( self, ms ):
  379. self.chordTester.stop()
  380. def pedal_down( self, ms ):
  381. print("pedal_down")
  382. self.midiDev.send_controller(64, 100 )
  383. def pedal_up( self, ms ):
  384. print("pedal_up");
  385. self.midiDev.send_controller(64, 0 )
  386. def all_notes_off(self, ms):
  387. self.api.all_notes_off();
  388. def check_for_errors(self,ms):
  389. self.api.check_for_serial_errors()
  390. def quit( self, ms ):
  391. if self.api:
  392. self.api.close()
  393. def _send_error( pipe, res ):
  394. if res is None:
  395. return
  396. if res.msg:
  397. pipe.send( [{"type":"error", 'value':res.msg}] )
  398. def _send_error_msg( pipe, msg ):
  399. _send_error( pipe, Result(None,msg))
  400. def _send_quit( pipe ):
  401. pipe.send( [{ 'type':'quit' }] )
  402. # This is the application engine async. process loop
  403. def app_event_loop_func( pipe, cfg ):
  404. multiprocessing.get_logger().info("App Proc Started.")
  405. # create the asynchronous application object
  406. app = App()
  407. res = app.setup(cfg)
  408. # if the app did not initialize successfully
  409. if not res:
  410. _send_error( pipe, res )
  411. _send_quit(pipe)
  412. return
  413. dt0 = datetime.now()
  414. ms = 0
  415. while True:
  416. # have any message arrived from the parent process?
  417. if pipe.poll():
  418. msg = None
  419. try:
  420. msg = pipe.recv()
  421. except EOFError:
  422. return
  423. if not hasattr(app,msg.type):
  424. _send_error_msg( pipe, "Unknown message type:'%s'." % (msg.type) )
  425. else:
  426. # get the command handler function in 'app'
  427. func = getattr(app,msg.type)
  428. ms = int(round( (datetime.now() - dt0).total_seconds() * 1000.0) )
  429. # call the command handler
  430. if msg.value:
  431. res = func( ms, msg.value )
  432. else:
  433. res = func( ms )
  434. # handle any errors returned from the commands
  435. _send_error( pipe, res )
  436. # if a 'quit' msg was recived then break out of the loop
  437. if msg.type == 'quit':
  438. _send_quit(pipe)
  439. break
  440. # give some time to the system
  441. time.sleep(0.05)
  442. # calc the tick() time stamp
  443. ms = int(round( (datetime.now() - dt0).total_seconds() * 1000.0) )
  444. # tick the app
  445. app.tick( ms )
  446. class AppProcess(Process):
  447. def __init__(self,cfg):
  448. self.parent_end, child_end = Pipe()
  449. super(AppProcess, self).__init__(target=app_event_loop_func,name="AppProcess",args=(child_end,cfg))
  450. self.doneFl = False
  451. def send(self, d):
  452. # This function is called by the parent process to send an arbitrary msg to the App process
  453. self.parent_end.send( types.SimpleNamespace(**d) )
  454. return None
  455. def recv(self):
  456. # This function is called by the parent process to receive lists of child messages.
  457. msgL = None
  458. if not self.doneFl and self.parent_end.poll():
  459. msgL = self.parent_end.recv()
  460. for msg in msgL:
  461. if msg['type'] == 'quit':
  462. self.doneFl = True
  463. return msgL
  464. def isdone(self):
  465. return self.doneFl
  466. class Shell:
  467. def __init__( self, cfg ):
  468. self.appProc = None
  469. self.parseD = {
  470. 'q':{ "func":'quit', "minN":0, "maxN":0, "help":"quit"},
  471. '?':{ "func":"_help", "minN":0, "maxN":0, "help":"Print usage text."},
  472. 'a':{ "func":"audio_dev_list", "minN":0, "maxN":0, "help":"List the audio devices."},
  473. 'm':{ "func":"midi_dev_list", "minN":0, "maxN":0, "help":"List the MIDI devices."},
  474. 'c':{ "func":"calibrate_keys_start", "minN":1, "maxN":2, "help":"Calibrate a range of keys. "},
  475. 'd':{ "func":"calibrate_start", "minN":1, "maxN":1, "help":"Calibrate based on fixed db levels. "},
  476. 'D':{ "func":"calibrate_play", "minN":1, "maxN":1, "help":"Play back last calibration."},
  477. 's':{ "func":"calibrate_keys_stop", "minN":0, "maxN":0, "help":"Stop key calibration"},
  478. 'p':{ "func":"play_keys_start", "minN":1, "maxN":2, "help":"Play current calibration"},
  479. 'k':{ "func":"keyboard_start_pulse_idx", "minN":3, "maxN":3, "help":"Play pulse index across keyboard"},
  480. 'r':{ "func":"keyboard_repeat_pulse_idx", "minN":1, "maxN":1, "help":"Repeat pulse index across keyboard with new pulse_idx"},
  481. 'K':{ "func":"keyboard_start_target_db", "minN":3, "maxN":3, "help":"Play db across keyboard"},
  482. 'R':{ "func":"keyboard_repeat_target_db", "minN":1, "maxN":1, "help":"Repeat db across keyboard with new pulse_idx"},
  483. 'F':{ "func":"midi_file_player_start", "minN":0, "maxN":0, "help":"Play the MIDI file."},
  484. 'f':{ "func":"midi_file_player_stop", "minN":0, "maxN":0, "help":"Stop the MIDI file."},
  485. 'V':{ "func":"vel_table_updown", "minN":2, "maxN":2, "help":"Play Velocity Table up/down - across."},
  486. 'A':{ "func":"vel_table_across", "minN":2, "maxN":2, "help":"Play Velocity Table across - up/down."},
  487. 'v':{ "func":"vel_table_stop", "minN":0, "maxN":0, "help":"Stop the velocity table playback."},
  488. 'N':{ "func":"note_tester_start", "minN":0, "maxN":0, "help":"Play a note using NoteTester."},
  489. 'n':{ "func":"note_tester_stop", "minN":0, "maxN":0, "help":"Stop NoteTester."},
  490. 'T':{ "func":"poly_note_tester_start", "minN":0, "maxN":0, "help":"Play random notes using PolyNoteTester."},
  491. 't':{ "func":"poly_note_tester_stop", "minN":0, "maxN":0, "help":"Stop NoteTester."},
  492. 'H':{ "func":"chord_tester_start", "minN":0, "maxN":0, "help":"Chord tester start."},
  493. 'h':{ "func":"chord_tester_stop", "minN":0, "maxN":0, "help":"Chord tester stop."},
  494. 'P':{ "func":"pedal_down", "minN":0, "maxN":0, "help":"Pedal down."},
  495. 'p':{ "func":"pedal_up", "minN":0, "maxN":0, "help":"Pedal up."},
  496. 'O':{ "func":"all_notes_off", "minN":0, "maxN":0, "help":"All notes off."},
  497. 'E':{ "func":"check_for_errors", "minN":0, "maxN":0, "help":"Check for errors."}
  498. }
  499. def _help( self, _=None ):
  500. for k,d in self.parseD.items():
  501. s = "{} = {}".format( k, d['help'] )
  502. print(s)
  503. return None
  504. def _syntaxError( self, msg ):
  505. return Result(None,"Syntax Error: " + msg )
  506. def _exec_cmd( self, tokL ):
  507. if len(tokL) <= 0:
  508. return None
  509. opcode = tokL[0]
  510. if opcode not in self.parseD:
  511. return self._syntaxError("Unknown opcode: '{}'.".format(opcode))
  512. d = self.parseD[ opcode ]
  513. func_name = d['func']
  514. func = None
  515. # find the function associated with this command
  516. if hasattr(self, func_name ):
  517. func = getattr(self, func_name )
  518. try:
  519. # convert the parameter list into integers
  520. argL = [ int(tokL[i]) for i in range(1,len(tokL)) ]
  521. except:
  522. return self._syntaxError("Unable to create integer arguments.")
  523. # validate the count of command args
  524. if d['minN'] != -1 and (d['minN'] > len(argL) or len(argL) > d['maxN']):
  525. return self._syntaxError("Argument count mismatch. {} is out of range:{} to {}".format(len(argL),d['minN'],d['maxN']))
  526. # call the command function
  527. if func:
  528. result = func(*argL)
  529. else:
  530. result = self.appProc.send( { 'type':func_name, 'value':argL } )
  531. return result
  532. def run( self ):
  533. # create the API object
  534. self.appProc = AppProcess(cfg)
  535. self.appProc.start()
  536. print("'q'=quit '?'=help")
  537. time_out_secs = 1
  538. # this is the shell main loop
  539. while True:
  540. # wait for keyboard activity
  541. i, o, e = select.select( [sys.stdin], [], [], time_out_secs )
  542. if i:
  543. # read the command
  544. s = sys.stdin.readline().strip()
  545. # tokenize the command
  546. tokL = s.split(' ')
  547. # execute the command
  548. result = self._exec_cmd( tokL )
  549. # if this is the 'quit' command
  550. if tokL[0] == 'q':
  551. break
  552. # check for msg's from the async application process
  553. if self._handle_app_msgs( self.appProc.recv() ):
  554. break
  555. # wait for the appProc to complete
  556. while not self.appProc.isdone():
  557. self.appProc.recv() # drain the AppProc() as it shutdown
  558. time.sleep(0.1)
  559. def _handle_app_msgs( self, msgL ):
  560. quitAppFl = False
  561. if msgL:
  562. for msg in msgL:
  563. if msg:
  564. if msg['type'] == 'error':
  565. print("Error: {}".format(msg['value']))
  566. elif msg['type'] == 'quit':
  567. quitAppFl = True
  568. else:
  569. print(msg)
  570. return quitAppFl
  571. def parse_args():
  572. """Parse the command line arguments."""
  573. descStr = """Picadae auto-calibrate."""
  574. logL = ['debug','info','warning','error','critical']
  575. ap = argparse.ArgumentParser(description=descStr)
  576. ap.add_argument("-c","--config", default="p_ac.yml", help="YAML configuration file.")
  577. ap.add_argument("-l","--log_level",choices=logL, default="warning", help="Set logging level: debug,info,warning,error,critical. Default:warning")
  578. return ap.parse_args()
  579. def create_logger():
  580. import multiprocessing, logging
  581. logger = multiprocessing.get_logger()
  582. logger.setLevel(logging.INFO)
  583. formatter = logging.Formatter(\
  584. '[%(asctime)s| %(levelname)s| %(processName)s] %(message)s')
  585. handler = logging.FileHandler('/home/kevin/temp/p_ac_log.txt')
  586. handler.setFormatter(formatter)
  587. # this bit will make sure you won't have
  588. # duplicated messages in the output
  589. if not len(logger.handlers):
  590. logger.addHandler(handler)
  591. return logger
  592. if __name__ == "__main__":
  593. from multiprocessing import Pool
  594. logger = create_logger()
  595. logger.info('Starting pooling')
  596. #p = Pool()
  597. #logging.basicConfig()
  598. #mplog = multiprocessing.log_to_stderr()
  599. #mplog.setLevel(logging.INFO)
  600. args = parse_args()
  601. cfg = parse_yaml_cfg(args.config)
  602. shell = Shell(cfg)
  603. shell.run()