Added calibrate.py, plot_calibrate.py and made associated changes.
Added plot_calibrate.ipynb jupyter notebook.
This commit is contained in:
parent
26b997811a
commit
fc1a0d8a61
222
MidiDevice.py
Normal file
222
MidiDevice.py
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import rtmidi
|
||||||
|
import rtmidi.midiutil
|
||||||
|
|
||||||
|
from result import Result
|
||||||
|
|
||||||
|
class MidiDevice(object):
|
||||||
|
def __init__(self, **kwargs ):
|
||||||
|
self.mip = None
|
||||||
|
self.mop = None
|
||||||
|
self.inMonitorFl = False
|
||||||
|
self.outMonitorFl = False
|
||||||
|
self.throughFl = False
|
||||||
|
self.inPortLabel = None
|
||||||
|
self.outPortLabel = None
|
||||||
|
self.setup(**kwargs)
|
||||||
|
|
||||||
|
def setup( self, **kwargs ):
|
||||||
|
|
||||||
|
res = Result()
|
||||||
|
|
||||||
|
if kwargs is None:
|
||||||
|
return res
|
||||||
|
|
||||||
|
if 'inPortLabel' in kwargs:
|
||||||
|
res += self.select_port( True, kwargs['inPortLabel'] )
|
||||||
|
|
||||||
|
if 'outPortLabel' in kwargs:
|
||||||
|
res += self.select_port( False, kwargs['outPortLabel'] )
|
||||||
|
|
||||||
|
if 'inMonitorFl' in kwargs:
|
||||||
|
self.enable_monitor( True, kwargs['inMonitorFl'] )
|
||||||
|
|
||||||
|
if 'outMonitorFl' in kwargs:
|
||||||
|
self.enable_monitor( True, kwargs['outMonitorFl'] )
|
||||||
|
|
||||||
|
if 'throughFl' in kwargs:
|
||||||
|
self.enable_through( kwargs['throughFl'] )
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _clean_port_label( self, portLabel ):
|
||||||
|
return ' '.join(portLabel.split(' ')[:-1])
|
||||||
|
|
||||||
|
def _get_port_list( self, inDirFl ):
|
||||||
|
dev = rtmidi.MidiIn() if inDirFl else rtmidi.MidiOut()
|
||||||
|
|
||||||
|
# get port list and drop the numeric id at the end of the port label
|
||||||
|
return [ self._clean_port_label(p) for p in dev.get_ports() ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_port_list( self, inDirFl ):
|
||||||
|
return { 'type':'midi',
|
||||||
|
'dir': 'in' if inDirFl else 'out',
|
||||||
|
'op': 'list',
|
||||||
|
'listL': self._get_port_list( inDirFl )
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_in_port_list( self ):
|
||||||
|
return self.get_port_list( True )
|
||||||
|
|
||||||
|
def get_out_port_list( self ):
|
||||||
|
return self.get_port_list( False )
|
||||||
|
|
||||||
|
def select_port( self, inDirFl, portLabel ):
|
||||||
|
res = Result()
|
||||||
|
|
||||||
|
if portLabel:
|
||||||
|
|
||||||
|
dirLabel = "input" if inDirFl else "output"
|
||||||
|
|
||||||
|
portL = self._get_port_list( inDirFl )
|
||||||
|
|
||||||
|
if portLabel not in portL:
|
||||||
|
res.set_error("The port '%s' is not an available %s port." % (portLabel,dirLabel))
|
||||||
|
else:
|
||||||
|
port_idx = portL.index(portLabel) # TODO error check
|
||||||
|
|
||||||
|
if inDirFl:
|
||||||
|
self.mip,self.inPortLabel = rtmidi.midiutil.open_midiinput(port=port_idx)
|
||||||
|
self.inPortLabel = self._clean_port_label(self.inPortLabel)
|
||||||
|
else:
|
||||||
|
self.mop,self.outPortLabel = rtmidi.midiutil.open_midioutput(port=port_idx)
|
||||||
|
self.outPortLabel = self._clean_port_label(self.outPortLabel)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
def select_in_port( self, portLabel ):
|
||||||
|
return self.select_port( True, portLabel )
|
||||||
|
|
||||||
|
def select_out_port( self, portLabel ):
|
||||||
|
return self.select_port( False, portLabel )
|
||||||
|
|
||||||
|
def enable_through( self, throughFl ):
|
||||||
|
self.throughFl = throughFl
|
||||||
|
|
||||||
|
def enable_monitor( self, inDirFl, monitorFl ):
|
||||||
|
if inDirFl:
|
||||||
|
self.inMonitorFl = monitorFl
|
||||||
|
else:
|
||||||
|
self.outMonitorFl = monitorFl
|
||||||
|
|
||||||
|
def enable_in_monitor( self, monitorFl):
|
||||||
|
self.enable_monitor( True, monitorFl )
|
||||||
|
|
||||||
|
def enable_out_monitor( self, monitorFl):
|
||||||
|
self.enable_monitor( False, monitorFl )
|
||||||
|
|
||||||
|
def port_name( self, inDirFl ):
|
||||||
|
return inPortLabel if inDirFl else outPortLabel
|
||||||
|
|
||||||
|
def in_port_name( self ):
|
||||||
|
return self.port_name(True)
|
||||||
|
|
||||||
|
def out_port_name( self ):
|
||||||
|
return self.port_name(False)
|
||||||
|
|
||||||
|
def _midi_data_to_text_msg( self, inFl, midi_data ):
|
||||||
|
|
||||||
|
text = ""
|
||||||
|
if len(midi_data) > 0:
|
||||||
|
text += "{:02x}".format(midi_data[0])
|
||||||
|
|
||||||
|
if len(midi_data) > 1:
|
||||||
|
text += " {:3d}".format(midi_data[1])
|
||||||
|
|
||||||
|
if len(midi_data) > 2:
|
||||||
|
text += " {:3d}".format(midi_data[2])
|
||||||
|
|
||||||
|
text = ("in: " if inFl else "out: ") + text
|
||||||
|
return { 'type':'midi', 'dir':inFl, 'op':'monitor', 'value':text }
|
||||||
|
|
||||||
|
def get_input( self ):
|
||||||
|
o_msgL = []
|
||||||
|
|
||||||
|
if self.mip is not None:
|
||||||
|
midi_msg = self.mip.get_message()
|
||||||
|
if midi_msg and midi_msg[0]:
|
||||||
|
|
||||||
|
if self.monitorInFl:
|
||||||
|
o_msgL.append( self._midi_data_to_text_msg(True,midi_msg[0]) )
|
||||||
|
|
||||||
|
if self.throughFl and self.mop is not None:
|
||||||
|
self.mop.send_message(midi_msg[0])
|
||||||
|
|
||||||
|
|
||||||
|
o_msgL.append( { 'type':'midi', 'op':'data', 'dir':'in', 'value':midi_msg[0] } )
|
||||||
|
|
||||||
|
return o_msgL
|
||||||
|
|
||||||
|
|
||||||
|
def send_output( self, m ):
|
||||||
|
o_msgL = []
|
||||||
|
|
||||||
|
if self.mop is not None:
|
||||||
|
self.mop.send_message(m)
|
||||||
|
|
||||||
|
if self.outMonitorFl:
|
||||||
|
o_msgL += [self._midi_data_to_text_msg( False, m )]
|
||||||
|
|
||||||
|
return o_msgL
|
||||||
|
|
||||||
|
def send_note_on( self, pitch, vel, ch=0 ):
|
||||||
|
return self.send_output( [ 0x90+ch, pitch, vel ] )
|
||||||
|
|
||||||
|
def send_note_off( self, pitch, ch=0 ):
|
||||||
|
return self.send_note_on( 0, ch )
|
||||||
|
|
||||||
|
def send_pgm_change( self, pgm, ch=0 ):
|
||||||
|
return self.send_output( [ 0xc0+ch, pgm ] )
|
||||||
|
|
||||||
|
def send_pbend( self, val, ch=0 ):
|
||||||
|
assert( val < 8192 )
|
||||||
|
|
||||||
|
ival = int(val)
|
||||||
|
|
||||||
|
lsb = ival & 0x7f
|
||||||
|
msb = (ival >> 7) & 0x7f
|
||||||
|
|
||||||
|
return self.send_output( [ 0xe0+ch, lsb, msb ] )
|
||||||
|
|
||||||
|
|
||||||
|
def send_controller( self, num, value, ch=0 ):
|
||||||
|
return self.send_output( [0xb0+ch, num, value ] )
|
||||||
|
|
||||||
|
def send_all_notes_off(self, ch=0 ):
|
||||||
|
return self.send_controller( 123, 1, ch=ch )
|
||||||
|
|
||||||
|
def get_state( self ):
|
||||||
|
return {
|
||||||
|
"inMonitorFl":self.inMonitorFl,
|
||||||
|
"outMonitorFl":self.outMonitorFl,
|
||||||
|
"throughFl":self.throughFl,
|
||||||
|
"inPortLabel":self.inPortLabel,
|
||||||
|
"outPortLabel":self.outPortLabel,
|
||||||
|
"inPortL":self.get_in_port_list(),
|
||||||
|
"outPortL":self.get_out_port_list()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def on_command( self, m, ms ):
|
||||||
|
errL = []
|
||||||
|
|
||||||
|
if m.type == 'midi':
|
||||||
|
if m.op == 'sel':
|
||||||
|
errL.append(self.select_port( m.dir=='in', m.value ))
|
||||||
|
|
||||||
|
elif m.op == 'through':
|
||||||
|
self.enable_through( m.value )
|
||||||
|
|
||||||
|
elif m.op == 'monitor':
|
||||||
|
self.enable_monitor( m.dir=='in', m.value )
|
||||||
|
|
||||||
|
return errL
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
md = MidiDevice()
|
||||||
|
|
||||||
|
print(md.get_port_list( True ))
|
||||||
|
print(md.get_port_list( False ))
|
||||||
|
|
427
calibrate.py
Normal file
427
calibrate.py
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
import os,types,wave,json,array
|
||||||
|
import numpy as np
|
||||||
|
from rms_analysis import rms_analyze_one_note
|
||||||
|
|
||||||
|
class Calibrate:
|
||||||
|
def __init__( self, cfg, audio, midi, api ):
|
||||||
|
self.cfg = types.SimpleNamespace(**cfg)
|
||||||
|
self.audio = audio
|
||||||
|
self.midi = midi
|
||||||
|
self.api = api
|
||||||
|
self.state = "stopped" # stopped | started | note_on | note_off | analyzing
|
||||||
|
self.playOnlyFl = False
|
||||||
|
self.startMs = None
|
||||||
|
self.nextStateChangeMs = None
|
||||||
|
self.curHoldDutyCyclePctD = None # { pitch:dutyPct}
|
||||||
|
self.noteAnnotationL = [] # (noteOnMs,noteOffMs,pitch,pulseUs)
|
||||||
|
|
||||||
|
self.measD = None # { midi_pitch: [ {pulseUs, db, durMs, targetDb } ] }
|
||||||
|
|
||||||
|
self.curNoteStartMs = None
|
||||||
|
self.curPitchIdx = None
|
||||||
|
self.curTargetDbIdx = None
|
||||||
|
self.successN = None
|
||||||
|
self.failN = None
|
||||||
|
|
||||||
|
self.curTargetDb = None
|
||||||
|
self.curPulseUs = None
|
||||||
|
self.curMatchN = None
|
||||||
|
self.curAttemptN = None
|
||||||
|
self.lastAudiblePulseUs = None
|
||||||
|
self.maxTooShortPulseUs = None
|
||||||
|
self.pulseDbL = None
|
||||||
|
self.deltaUpMult = None
|
||||||
|
self.deltaDnMult = None
|
||||||
|
self.skipMeasFl = None
|
||||||
|
|
||||||
|
def start(self,ms):
|
||||||
|
self.stop(ms)
|
||||||
|
self.state = 'started'
|
||||||
|
self.playOnlyFl = False
|
||||||
|
self.nextStateChangeMs = ms + 500
|
||||||
|
|
||||||
|
self.startMs = ms
|
||||||
|
|
||||||
|
self.curPitchIdx = 0
|
||||||
|
self.curPulseUs = self.cfg.initPulseUs
|
||||||
|
self.lastAudiblePulseUs = None
|
||||||
|
self.maxTooShortPulseUs = None
|
||||||
|
self.pulseDbL = []
|
||||||
|
self.deltaUpMult = 1
|
||||||
|
self.deltaDnMult = 1
|
||||||
|
self.curTargetDbIdx = -1
|
||||||
|
self._start_new_db_target()
|
||||||
|
|
||||||
|
self.curDutyPctD = {}
|
||||||
|
self.skipMeasFl = False
|
||||||
|
self.measD = {}
|
||||||
|
|
||||||
|
self.successN = 0
|
||||||
|
self.failN = 0
|
||||||
|
self.audio.record_enable(True)
|
||||||
|
|
||||||
|
def stop(self,ms):
|
||||||
|
|
||||||
|
if self.midi is not None:
|
||||||
|
self.midi.send_all_notes_off()
|
||||||
|
|
||||||
|
if not self.playOnlyFl:
|
||||||
|
self.audio.record_enable(False)
|
||||||
|
|
||||||
|
self._save_results()
|
||||||
|
|
||||||
|
def play(self,ms):
|
||||||
|
|
||||||
|
if self.measD is None or len(self.measD) == 0:
|
||||||
|
print("Nothing to play.")
|
||||||
|
else:
|
||||||
|
self.state = 'started'
|
||||||
|
self.playOnlyFl = True
|
||||||
|
self.nextStateChangeMs = ms + 500
|
||||||
|
self.curPitchIdx = -1
|
||||||
|
self.curTargetDbIdx = 0
|
||||||
|
self._do_play_update()
|
||||||
|
|
||||||
|
def tick(self,ms):
|
||||||
|
|
||||||
|
if self.nextStateChangeMs is not None and ms > self.nextStateChangeMs:
|
||||||
|
|
||||||
|
if self.state == 'stopped':
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif self.state == 'started':
|
||||||
|
self._do_note_on(ms)
|
||||||
|
self.nextStateChangeMs += self.cfg.noteOnDurMs
|
||||||
|
self.state = 'note_on'
|
||||||
|
|
||||||
|
elif self.state == 'note_on':
|
||||||
|
self._do_note_off(ms)
|
||||||
|
self.nextStateChangeMs += self.cfg.noteOffDurMs
|
||||||
|
self.state = 'note_off'
|
||||||
|
|
||||||
|
elif self.state == 'note_off':
|
||||||
|
if self.playOnlyFl:
|
||||||
|
if not self._do_play_update():
|
||||||
|
self.state = 'stopped'
|
||||||
|
else:
|
||||||
|
if self._do_analysis(ms):
|
||||||
|
if not self._start_new_db_target():
|
||||||
|
self.stop(ms)
|
||||||
|
self.state = 'stopped'
|
||||||
|
print("DONE!")
|
||||||
|
|
||||||
|
# if the state was not changed to 'stopped'
|
||||||
|
if self.state == 'note_off':
|
||||||
|
self.state = 'started'
|
||||||
|
|
||||||
|
|
||||||
|
def _do_play_update( self ):
|
||||||
|
|
||||||
|
self.curPitchIdx +=1
|
||||||
|
if self.curPitchIdx >= len(self.cfg.pitchL):
|
||||||
|
self.curPitchIdx = 0
|
||||||
|
self.curTargetDbIdx += 1
|
||||||
|
if self.curTargetDbIdx >= len(self.cfg.targetDbL):
|
||||||
|
return False
|
||||||
|
|
||||||
|
pitch = self.cfg.pitchL[ self.curPitchIdx ]
|
||||||
|
targetDb = self.cfg.targetDbL[ self.curTargetDbIdx ]
|
||||||
|
self.curPulseUs = -1
|
||||||
|
for d in self.measD[ pitch ]:
|
||||||
|
if d['targetDb'] == targetDb and d['matchFl']==True:
|
||||||
|
self.curPulseUs = d['pulse_us']
|
||||||
|
break
|
||||||
|
|
||||||
|
if self.curPulseUs == -1:
|
||||||
|
print("Pitch:%i TargetDb:%f not found." % (pitch,targetDb))
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("Target db: %4.1f" % (targetDb))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _get_duty_cycle( self, pitch, pulseUsec ):
|
||||||
|
|
||||||
|
dutyPct = 50
|
||||||
|
|
||||||
|
if pitch in self.cfg.holdDutyPctD:
|
||||||
|
|
||||||
|
dutyPct = self.cfg.holdDutyPctD[pitch][0][1]
|
||||||
|
for refUsec,refDuty in self.cfg.holdDutyPctD[pitch]:
|
||||||
|
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)
|
||||||
|
self.skipMeasFl = True
|
||||||
|
|
||||||
|
return dutyPct
|
||||||
|
|
||||||
|
def _do_note_on(self,ms):
|
||||||
|
self.curNoteStartMs = ms
|
||||||
|
|
||||||
|
pitch = self.cfg.pitchL[ self.curPitchIdx]
|
||||||
|
|
||||||
|
if self.midi is not None:
|
||||||
|
self.midi.send_note_on( pitch, 60 )
|
||||||
|
else:
|
||||||
|
self._set_duty_cycle( pitch, self.curPulseUs )
|
||||||
|
self.api.note_on_us( pitch, self.curPulseUs )
|
||||||
|
|
||||||
|
|
||||||
|
print("note-on: ",pitch," ",self.curPulseUs," us")
|
||||||
|
|
||||||
|
def _do_note_off(self,ms):
|
||||||
|
self.noteAnnotationL.append( { 'beg_ms':self.curNoteStartMs-self.startMs, 'end_ms':ms-self.startMs, 'midi_pitch':self.cfg.pitchL[ self.curPitchIdx], 'pulse_us':self.curPulseUs } )
|
||||||
|
|
||||||
|
if self.midi is not None:
|
||||||
|
self.midi.send_note_off( self.cfg.pitchL[ self.curPitchIdx] )
|
||||||
|
else:
|
||||||
|
for pitch in self.cfg.pitchL:
|
||||||
|
self.api.note_off( pitch )
|
||||||
|
|
||||||
|
|
||||||
|
#print("note-off: ",self.cfg.pitchL[ self.curPitchIdx])
|
||||||
|
|
||||||
|
|
||||||
|
def _calc_next_pulse_us( self, targetDb ):
|
||||||
|
|
||||||
|
# sort pulseDb ascending on db
|
||||||
|
self.pulseDbL = sorted( self.pulseDbL, key=lambda x: x[1] )
|
||||||
|
|
||||||
|
|
||||||
|
pulseL,dbL = zip(*self.pulseDbL)
|
||||||
|
|
||||||
|
max_i = np.argmax(dbL)
|
||||||
|
min_i = np.argmin(dbL)
|
||||||
|
|
||||||
|
if targetDb > dbL[max_i]:
|
||||||
|
pu = pulseL[max_i] + self.deltaUpMult * 500
|
||||||
|
self.deltaUpMult += 1
|
||||||
|
|
||||||
|
elif targetDb < dbL[min_i]:
|
||||||
|
pu = pulseL[min_i] - self.deltaDnMult * 500
|
||||||
|
self.deltaDnMult += 1
|
||||||
|
if self.maxTooShortPulseUs is not None and pu < self.maxTooShortPulseUs:
|
||||||
|
# BUG: this is a problem is self.pulseL[min_i] is <= than self.maxTooShortPulseUs
|
||||||
|
# the abs() covers the problem to prevent decreasing from maxTooShortPulseus
|
||||||
|
pu = self.maxTooShortPulseUs + (abs(pulseL[min_i] - self.maxTooShortPulseUs))/2
|
||||||
|
self.deltaDnMult = 1
|
||||||
|
else:
|
||||||
|
self.deltaUpMult = 1
|
||||||
|
self.deltaDnMult = 1
|
||||||
|
pu = np.interp([targetDb],dbL,pulseL)
|
||||||
|
|
||||||
|
return max(min(pu,self.cfg.maxPulseUs),self.cfg.minPulseUs)
|
||||||
|
|
||||||
|
def _do_analysis(self,ms):
|
||||||
|
|
||||||
|
analysisDoneFl = False
|
||||||
|
midi_pitch = self.cfg.pitchL[self.curPitchIdx]
|
||||||
|
pulse_us = self.curPulseUs
|
||||||
|
|
||||||
|
measD = self._meas_note(midi_pitch,pulse_us)
|
||||||
|
|
||||||
|
# if the the 'skip' flag is set then don't analyze this note
|
||||||
|
if self.skipMeasFl:
|
||||||
|
self.skipMeasFl = False
|
||||||
|
print("SKIP")
|
||||||
|
else:
|
||||||
|
|
||||||
|
db = measD[self.cfg.dbSrcLabel]['db']
|
||||||
|
durMs = measD['hm']['durMs']
|
||||||
|
|
||||||
|
# if this note is shorter than the minimum allowable duration
|
||||||
|
if durMs < self.cfg.minMeasDurMs:
|
||||||
|
|
||||||
|
print("SHORT!")
|
||||||
|
|
||||||
|
if self.maxTooShortPulseUs is None or self.curPulseUs > self.maxTooShortPulseUs:
|
||||||
|
self.maxTooShortPulseUs = self.curPulseUs
|
||||||
|
|
||||||
|
if self.lastAudiblePulseUs is not None and self.curPulseUs < self.lastAudiblePulseUs:
|
||||||
|
self.curPulseUs = self.lastAudiblePulseUs
|
||||||
|
else:
|
||||||
|
self.curPulseUs = self.cfg.initPulseUs
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
# this is a valid measurement store it to the pulse-db table
|
||||||
|
self.pulseDbL.append( (self.curPulseUs,db) )
|
||||||
|
|
||||||
|
# track the most recent audible note - to return to if a successive note is too short
|
||||||
|
self.lastAudiblePulseUs = self.curPulseUs
|
||||||
|
|
||||||
|
# calc the upper and lower bounds db range
|
||||||
|
lwr_db = self.curTargetDb * ((100.0 - self.cfg.tolDbPct)/100.0)
|
||||||
|
upr_db = self.curTargetDb * ((100.0 + self.cfg.tolDbPct)/100.0)
|
||||||
|
|
||||||
|
# was this note is inside the db range then set the 'match' flag
|
||||||
|
if lwr_db <= db and db <= upr_db:
|
||||||
|
self.curMatchN += 1
|
||||||
|
measD['matchFl'] = True
|
||||||
|
print("MATCH!")
|
||||||
|
|
||||||
|
#
|
||||||
|
self.curPulseUs = int(self._calc_next_pulse_us(self.curTargetDb))
|
||||||
|
|
||||||
|
# if at least minMatchN matches have been made on this pitch/targetDb
|
||||||
|
if self.curMatchN >= self.cfg.minMatchN:
|
||||||
|
analysisDoneFl = True
|
||||||
|
self.successN += 1
|
||||||
|
print("Anysis Done: Success")
|
||||||
|
|
||||||
|
# if at least maxAttemptN match attempts have been made without success
|
||||||
|
self.curAttemptN += 1
|
||||||
|
if self.curAttemptN >= self.cfg.maxAttemptN:
|
||||||
|
analysisDoneFl = True
|
||||||
|
self.failN += 1
|
||||||
|
print("Analysis Done: Fail")
|
||||||
|
|
||||||
|
|
||||||
|
if midi_pitch not in self.measD:
|
||||||
|
self.measD[ midi_pitch ] = []
|
||||||
|
|
||||||
|
self.measD[ midi_pitch ].append( measD )
|
||||||
|
|
||||||
|
return analysisDoneFl
|
||||||
|
|
||||||
|
|
||||||
|
def _meas_note(self,midi_pitch,pulse_us):
|
||||||
|
|
||||||
|
# get the annotation information for the last note
|
||||||
|
annD = self.noteAnnotationL[-1]
|
||||||
|
|
||||||
|
buf_result = self.audio.linear_buffer()
|
||||||
|
|
||||||
|
if buf_result:
|
||||||
|
|
||||||
|
sigV = buf_result.value
|
||||||
|
|
||||||
|
# get the annotated begin and end of the note as sample indexes into sigV
|
||||||
|
bi = int(round(annD['beg_ms'] * self.audio.srate / 1000))
|
||||||
|
ei = int(round(annD['end_ms'] * self.audio.srate / 1000))
|
||||||
|
|
||||||
|
# calculate half the length of the note-off duration in samples
|
||||||
|
noteOffSmp_o_2 = int(round(self.cfg.noteOffDurMs/2 * self.audio.srate / 1000))
|
||||||
|
|
||||||
|
# widen the note analysis space noteOffSmp_o_2 samples pre/post the annotated begin/end of the note
|
||||||
|
bi = max(0,bi - noteOffSmp_o_2)
|
||||||
|
ei = min(noteOffSmp_o_2,sigV.shape[0]-1)
|
||||||
|
|
||||||
|
|
||||||
|
ar = types.SimpleNamespace(**self.cfg.analysisD)
|
||||||
|
|
||||||
|
# shift the annotatd begin/end of the note to be relative to index bi
|
||||||
|
begMs = noteOffSmp_o_2 * 1000 / self.audio.srate
|
||||||
|
endMs = begMs + (annD['end_ms'] - annD['beg_ms'])
|
||||||
|
|
||||||
|
# analyze the note
|
||||||
|
resD = rms_analyze_rt_one_note( sigV[bi:ei], self.audio.srate, begMs, endMs, midi_pitch, rmsWndMs=ar.rmsWndMs, rmsHopMs=ar.rmsHopMs, dbRefWndMs=ar.dbRefWndMs, harmCandN=ar.harmCandN, harmN=ar.harmN, durDecayPct=ar.durDecayPct )
|
||||||
|
|
||||||
|
resD["pulse_us"] = pulse_us
|
||||||
|
resD["midi_pitch"] = midi_pitch
|
||||||
|
resD["beg_ms"] = annD['beg_ms']
|
||||||
|
resD['end_ms'] = annD['end_ms']
|
||||||
|
resD['skipMeasFl'] = self.skipMeasFl
|
||||||
|
resD['matchFl'] = False
|
||||||
|
resD['targetDb'] = self.curTargetDb
|
||||||
|
resD['annIdx'] = len(self.noteAnnotationL)-1
|
||||||
|
|
||||||
|
print( "%4.1f hm:%4.1f (%4.1f) %4i td:%4.1f (%4.1f) %4i" % (self.curTargetDb,resD['hm']['db'], resD['hm']['db']-self.curTargetDb, resD['hm']['durMs'], resD['td']['db'], resD['td']['db']-self.curTargetDb, resD['td']['durMs']))
|
||||||
|
|
||||||
|
|
||||||
|
return resD
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _start_new_db_target(self):
|
||||||
|
|
||||||
|
self.curTargetDbIdx += 1
|
||||||
|
|
||||||
|
# if all db targets have been queried then advance to the next pitch
|
||||||
|
if self.curTargetDbIdx >= len(self.cfg.targetDbL):
|
||||||
|
|
||||||
|
self.curTargetDbIdx = 0
|
||||||
|
self.curPitchIdx += 1
|
||||||
|
|
||||||
|
# if all pitches have been queried then we are done
|
||||||
|
if self.curPitchIdx >= len(self.cfg.pitchL):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
self.curTargetDb = self.cfg.targetDbL[ self.curTargetDbIdx ]
|
||||||
|
self.curMatchN = 0
|
||||||
|
self.curAttemptN = 0
|
||||||
|
self.lastAudiblePulseUs = None
|
||||||
|
self.maxTooShortPulseUs = None
|
||||||
|
self.pulseDbL = []
|
||||||
|
self.deltaUpMult = 1
|
||||||
|
self.deltaDnMult = 1
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _write_16_bit_wav_file( self, fn ):
|
||||||
|
|
||||||
|
srate = int(self.audio.srate)
|
||||||
|
|
||||||
|
buf_result = self.audio.linear_buffer()
|
||||||
|
|
||||||
|
sigV = buf_result.value
|
||||||
|
|
||||||
|
smpN = sigV.shape[0]
|
||||||
|
chN = 1
|
||||||
|
sigV = np.squeeze(sigV.reshape( smpN * chN, )) * 0x7fff
|
||||||
|
sigL = [ int(round(sigV[i])) for i in range(smpN) ]
|
||||||
|
|
||||||
|
sigA = array.array('h',sigL)
|
||||||
|
|
||||||
|
with wave.open( fn, "wb") as f:
|
||||||
|
|
||||||
|
bits = 16
|
||||||
|
bits_per_byte = 8
|
||||||
|
f.setparams((chN, bits//bits_per_byte, srate, 0, 'NONE', 'not compressed'))
|
||||||
|
|
||||||
|
f.writeframes(sigA)
|
||||||
|
|
||||||
|
def _save_results( self ):
|
||||||
|
|
||||||
|
if self.measD is None or len(self.measD) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
outDir = os.path.expanduser( self.cfg.outDir )
|
||||||
|
|
||||||
|
if not os.path.isdir(outDir):
|
||||||
|
os.mkdir(outDir)
|
||||||
|
|
||||||
|
outDir = os.path.join( outDir, self.cfg.outLabel )
|
||||||
|
|
||||||
|
if not os.path.isdir(outDir):
|
||||||
|
os.mkdir(outDir)
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while( os.path.isdir( os.path.join(outDir,"%i" % i )) ):
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
outDir = os.path.join( outDir, "%i" % i )
|
||||||
|
os.mkdir(outDir)
|
||||||
|
|
||||||
|
self._write_16_bit_wav_file( os.path.join(outDir,"audio.wav"))
|
||||||
|
|
||||||
|
d = {'cfg':self.cfg.__dict__, 'measD': self.measD, 'annoteL':self.noteAnnotationL }
|
||||||
|
|
||||||
|
with open( os.path.join(outDir,"meas.json"), "w") as f:
|
||||||
|
json.dump(d,f)
|
||||||
|
|
||||||
|
|
73
p_ac.py
73
p_ac.py
@ -6,12 +6,14 @@ from multiprocessing import Process, Pipe
|
|||||||
|
|
||||||
from picadae_api import Picadae
|
from picadae_api import Picadae
|
||||||
from AudioDevice import AudioDevice
|
from AudioDevice import AudioDevice
|
||||||
|
from MidiDevice import MidiDevice
|
||||||
from result import Result
|
from result import Result
|
||||||
from common import parse_yaml_cfg
|
from common import parse_yaml_cfg
|
||||||
from plot_seq import form_resample_pulse_time_list
|
from plot_seq import form_resample_pulse_time_list
|
||||||
from plot_seq import form_final_pulse_list
|
from plot_seq import form_final_pulse_list
|
||||||
from rt_note_analysis import RT_Analyzer
|
from rt_note_analysis import RT_Analyzer
|
||||||
from keyboard import Keyboard
|
from keyboard import Keyboard
|
||||||
|
from calibrate import Calibrate
|
||||||
|
|
||||||
class AttackPulseSeq:
|
class AttackPulseSeq:
|
||||||
""" Sequence a fixed chord over a list of attack pulse lengths."""
|
""" Sequence a fixed chord over a list of attack pulse lengths."""
|
||||||
@ -274,13 +276,15 @@ class App:
|
|||||||
self.cfg = None
|
self.cfg = None
|
||||||
self.audioDev = None
|
self.audioDev = None
|
||||||
self.api = None
|
self.api = None
|
||||||
self.calibrate = None
|
self.cal_keys = None
|
||||||
self.keyboard = None
|
self.keyboard = None
|
||||||
|
self.calibrate = None
|
||||||
|
|
||||||
def setup( self, cfg ):
|
def setup( self, cfg ):
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
|
|
||||||
self.audioDev = AudioDevice()
|
self.audioDev = AudioDevice()
|
||||||
|
self.midiDev = MidiDevice()
|
||||||
|
|
||||||
#
|
#
|
||||||
# TODO: unify the result error handling
|
# TODO: unify the result error handling
|
||||||
@ -292,20 +296,31 @@ class App:
|
|||||||
if not res:
|
if not res:
|
||||||
self.audio_dev_list(0)
|
self.audio_dev_list(0)
|
||||||
else:
|
else:
|
||||||
self.api = Picadae( key_mapL=cfg.key_mapL)
|
|
||||||
|
|
||||||
# wait for the letter 'a' to come back from the serial port
|
if hasattr(cfg,'midi'):
|
||||||
api_res = self.api.wait_for_serial_sync(timeoutMs=cfg.serial_sync_timeout_ms)
|
res = self.midiDev.setup(**cfg.midi)
|
||||||
|
|
||||||
# did the serial port sync fail?
|
if not res:
|
||||||
if not api_res:
|
self.midi_dev_list(0)
|
||||||
res.set_error("Serial port sync failed.")
|
|
||||||
else:
|
else:
|
||||||
print("Serial port sync'ed")
|
self.midiDev = None
|
||||||
|
|
||||||
self.calibrate = CalibrateKeys( cfg, self.audioDev, self.api )
|
self.api = Picadae( key_mapL=cfg.key_mapL)
|
||||||
|
|
||||||
self.keyboard = Keyboard( cfg, self.audioDev, self.api )
|
# 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)
|
||||||
|
|
||||||
|
# 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 )
|
||||||
|
|
||||||
|
self.keyboard = Keyboard( cfg, self.audioDev, self.api )
|
||||||
|
|
||||||
|
self.calibrate = Calibrate( cfg.calibrateArgs, self.audioDev, self.midiDev, self.api )
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@ -313,10 +328,14 @@ class App:
|
|||||||
|
|
||||||
self.audioDev.tick(ms)
|
self.audioDev.tick(ms)
|
||||||
|
|
||||||
if self.calibrate:
|
if self.cal_keys:
|
||||||
self.calibrate.tick(ms)
|
self.cal_keys.tick(ms)
|
||||||
|
|
||||||
if self.keyboard:
|
if self.keyboard:
|
||||||
self.keyboard.tick(ms)
|
self.keyboard.tick(ms)
|
||||||
|
|
||||||
|
if self.calibrate:
|
||||||
|
self.calibrate.tick(ms)
|
||||||
|
|
||||||
def audio_dev_list( self, ms ):
|
def audio_dev_list( self, ms ):
|
||||||
portL = self.audioDev.get_port_list( True )
|
portL = self.audioDev.get_port_list( True )
|
||||||
@ -324,13 +343,25 @@ class App:
|
|||||||
for port in portL:
|
for port in portL:
|
||||||
print("chs:%4i label:%s" % (port['chN'],port['label']))
|
print("chs:%4i label:%s" % (port['chN'],port['label']))
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
def calibrate_keys_start( self, ms, pitchRangeL ):
|
def calibrate_keys_start( self, ms, pitchRangeL ):
|
||||||
chordL = [ [pitch] for pitch in range(pitchRangeL[0], pitchRangeL[1]+1)]
|
chordL = [ [pitch] for pitch in range(pitchRangeL[0], pitchRangeL[1]+1)]
|
||||||
self.calibrate.start( ms, chordL, cfg.full_pulseL )
|
self.cal_keys.start( ms, chordL, cfg.full_pulseL )
|
||||||
|
|
||||||
def play_keys_start( self, ms, pitchRangeL ):
|
def play_keys_start( self, ms, pitchRangeL ):
|
||||||
chordL = [ [pitch] for pitch in range(pitchRangeL[0], pitchRangeL[1]+1)]
|
chordL = [ [pitch] for pitch in range(pitchRangeL[0], pitchRangeL[1]+1)]
|
||||||
self.calibrate.start( ms, chordL, cfg.full_pulseL, playOnlyFl=True )
|
self.cal_keys.start( ms, chordL, cfg.full_pulseL, playOnlyFl=True )
|
||||||
|
|
||||||
def keyboard_start_pulse_idx( self, ms, argL ):
|
def keyboard_start_pulse_idx( self, ms, argL ):
|
||||||
pitchL = [ pitch for pitch in range(argL[0], argL[1]+1)]
|
pitchL = [ pitch for pitch in range(argL[0], argL[1]+1)]
|
||||||
@ -346,9 +377,16 @@ class App:
|
|||||||
def keyboard_repeat_target_db( self, ms, argL ):
|
def keyboard_repeat_target_db( self, ms, argL ):
|
||||||
self.keyboard.repeat( ms, None, argL[0] )
|
self.keyboard.repeat( ms, None, argL[0] )
|
||||||
|
|
||||||
|
def calibrate_start( self, ms, argL ):
|
||||||
|
self.calibrate.start(ms)
|
||||||
|
|
||||||
|
def calibrate_play( self, ms, argL ):
|
||||||
|
self.calibrate.play(ms)
|
||||||
|
|
||||||
def calibrate_keys_stop( self, ms ):
|
def calibrate_keys_stop( self, ms ):
|
||||||
self.calibrate.stop(ms)
|
self.cal_keys.stop(ms)
|
||||||
self.keyboard.stop(ms)
|
self.keyboard.stop(ms)
|
||||||
|
self.calibrate.stop(ms)
|
||||||
|
|
||||||
def quit( self, ms ):
|
def quit( self, ms ):
|
||||||
if self.api:
|
if self.api:
|
||||||
@ -424,7 +462,7 @@ def app_event_loop_func( pipe, cfg ):
|
|||||||
|
|
||||||
|
|
||||||
# give some time to the system
|
# give some time to the system
|
||||||
time.sleep(0.1)
|
time.sleep(0.05)
|
||||||
|
|
||||||
# calc the tick() time stamp
|
# calc the tick() time stamp
|
||||||
ms = int(round( (datetime.now() - dt0).total_seconds() * 1000.0) )
|
ms = int(round( (datetime.now() - dt0).total_seconds() * 1000.0) )
|
||||||
@ -470,7 +508,10 @@ class Shell:
|
|||||||
'q':{ "func":'quit', "minN":0, "maxN":0, "help":"quit"},
|
'q':{ "func":'quit', "minN":0, "maxN":0, "help":"quit"},
|
||||||
'?':{ "func":"_help", "minN":0, "maxN":0, "help":"Print usage text."},
|
'?':{ "func":"_help", "minN":0, "maxN":0, "help":"Print usage text."},
|
||||||
'a':{ "func":"audio_dev_list", "minN":0, "maxN":0, "help":"List the audio devices."},
|
'a':{ "func":"audio_dev_list", "minN":0, "maxN":0, "help":"List the audio devices."},
|
||||||
|
'm':{ "func":"midi_dev_list", "minN":0, "maxN":0, "help":"List the MIDI devices."},
|
||||||
'c':{ "func":"calibrate_keys_start", "minN":1, "maxN":2, "help":"Calibrate a range of keys. "},
|
'c':{ "func":"calibrate_keys_start", "minN":1, "maxN":2, "help":"Calibrate a range of keys. "},
|
||||||
|
'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."},
|
||||||
's':{ "func":"calibrate_keys_stop", "minN":0, "maxN":0, "help":"Stop key calibration"},
|
'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"},
|
'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"},
|
'k':{ "func":"keyboard_start_pulse_idx", "minN":3, "maxN":3, "help":"Play pulse index across keyboard"},
|
||||||
|
127
p_ac.yml
127
p_ac.yml
@ -6,8 +6,17 @@
|
|||||||
audio: {
|
audio: {
|
||||||
inPortLabel: "5 USB Audio CODEC:", #"HDA Intel PCH: CS4208", # "5 USB Audio CODEC:", #"5 USB Sound Device",
|
inPortLabel: "5 USB Audio CODEC:", #"HDA Intel PCH: CS4208", # "5 USB Audio CODEC:", #"5 USB Sound Device",
|
||||||
outPortLabel: ,
|
outPortLabel: ,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
midi_off: {
|
||||||
|
inMonitorFl: False,
|
||||||
|
outMonitorFl: False,
|
||||||
|
throughFl: False,
|
||||||
|
inPortLabel: "Fastlane:Fastlane MIDI A",
|
||||||
|
outPortLabel: "Fastlane:Fastlane MIDI A"
|
||||||
|
#inPortLabel: "picadae:picadae MIDI 1",
|
||||||
|
#outPortLabel: "picadae:picadae MIDI 1"
|
||||||
|
},
|
||||||
|
|
||||||
# Picadae API args
|
# Picadae API args
|
||||||
serial_dev: "/dev/ttyACM0",
|
serial_dev: "/dev/ttyACM0",
|
||||||
@ -61,6 +70,121 @@
|
|||||||
finalPulseListCacheFn: "/home/kevin/temp/final_pulse_list_cache.pickle",
|
finalPulseListCacheFn: "/home/kevin/temp/final_pulse_list_cache.pickle",
|
||||||
rmsAnalysisCacheFn: "/home/kevin/temp/rms_analysis_cache.pickle"
|
rmsAnalysisCacheFn: "/home/kevin/temp/rms_analysis_cache.pickle"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
calibrateArgs: {
|
||||||
|
|
||||||
|
outDir: "~/temp/calib0",
|
||||||
|
outLabel: "test",
|
||||||
|
|
||||||
|
analysisD: {
|
||||||
|
rmsWndMs: 300, # length of the RMS measurment window
|
||||||
|
rmsHopMs: 30, # RMS measurement inter window distance
|
||||||
|
dbRefWndMs: 500, # length of initial portion of signal to use to calculate the dB reference level
|
||||||
|
harmCandN: 5, # count of harmonic candidates to locate during harmonic based RMS analysis
|
||||||
|
harmN: 3, # count of harmonics to use to calculate harmonic based RMS analysis
|
||||||
|
durDecayPct: 40 # percent drop in RMS to indicate the end of a note
|
||||||
|
},
|
||||||
|
|
||||||
|
noteOnDurMs: 1000,
|
||||||
|
noteOffDurMs: 1000,
|
||||||
|
|
||||||
|
|
||||||
|
pitchL: [ 50, 51, 52 ], # list of pitches
|
||||||
|
targetDbL: [ 16, 20, 23 ], # list of target db
|
||||||
|
|
||||||
|
minMeasDurMs: 800, # minimum candidate note duration
|
||||||
|
tolDbPct: 5.0, # tolerance as a percent of targetDb above/below used to form match db window
|
||||||
|
maxPulseUs: 45000, # max. allowable pulse us
|
||||||
|
minPulseUs: 8000, # min. allowable pulse us
|
||||||
|
initPulseUs: 15000, # pulseUs for first note
|
||||||
|
minMatchN: 3, # at least 3 candidate notes must be within tolDbPct to move on to a new targetDb
|
||||||
|
maxAttemptN: 30, # give up if more than 20 candidate notes fail for a given targetDb
|
||||||
|
dbSrcLabel: 'td', # source of the db measurement 'td' (time-domain) or 'hm' (harmonic)
|
||||||
|
|
||||||
|
holdDutyPctD: {
|
||||||
|
23: [[0, 70]],
|
||||||
|
24: [[0, 75]],
|
||||||
|
25: [[0, 70]],
|
||||||
|
26: [[0, 65]],
|
||||||
|
27: [[0, 70]],
|
||||||
|
28: [[0, 70]],
|
||||||
|
29: [[0, 65]],
|
||||||
|
30: [[0, 65]],
|
||||||
|
31: [[0, 65]],
|
||||||
|
32: [[0, 60]],
|
||||||
|
33: [[0, 65]],
|
||||||
|
34: [[0, 65]],
|
||||||
|
35: [[0, 65]],
|
||||||
|
36: [[0, 65]],
|
||||||
|
37: [[0, 65]],
|
||||||
|
38: [[0, 60]],
|
||||||
|
39: [[0, 60]],
|
||||||
|
40: [[0, 55]],
|
||||||
|
41: [[0, 60]],
|
||||||
|
42: [[0, 60]],
|
||||||
|
43: [[0, 65]],
|
||||||
|
44: [[0, 60]],
|
||||||
|
45: [[0, 60]],
|
||||||
|
46: [[0, 60]],
|
||||||
|
47: [[0, 60]],
|
||||||
|
48: [[0, 70]],
|
||||||
|
49: [[0, 60]],
|
||||||
|
50: [[0, 50]],
|
||||||
|
51: [[0, 50]],
|
||||||
|
52: [[0, 55]],
|
||||||
|
53: [[0, 50]],
|
||||||
|
54: [[0, 50]],
|
||||||
|
55: [[0, 50], [22000, 55]],
|
||||||
|
56: [[0, 50]],
|
||||||
|
57: [[0, 50]],
|
||||||
|
58: [[0, 50]],
|
||||||
|
59: [[0, 60]],
|
||||||
|
60: [[0, 50]],
|
||||||
|
61: [[0, 50]],
|
||||||
|
62: [[0, 55]],
|
||||||
|
63: [[0, 50]],
|
||||||
|
64: [[0, 50]],
|
||||||
|
65: [[0, 50], [17000, 65]],
|
||||||
|
66: [[0, 53]],
|
||||||
|
67: [[0, 55]],
|
||||||
|
68: [[0, 53]],
|
||||||
|
69: [[0, 55]],
|
||||||
|
70: [[0, 50]],
|
||||||
|
71: [[0, 50]],
|
||||||
|
72: [[0, 60]],
|
||||||
|
73: [[0, 50]],
|
||||||
|
74: [[0, 60]],
|
||||||
|
75: [[0, 55]],
|
||||||
|
76: [[0, 70]],
|
||||||
|
77: [[0, 50], [15000, 60], [19000, 70]],
|
||||||
|
78: [[0, 60]],
|
||||||
|
79: [[0, 50], [15000, 60], [19000, 70]],
|
||||||
|
80: [[0, 45]],
|
||||||
|
81: [[0, 50], [15000, 70]],
|
||||||
|
82: [[0, 50], [12500, 60], [14000, 65], [17000, 70]],
|
||||||
|
83: [[0, 50], [15000, 65]],
|
||||||
|
84: [[0, 50], [12500, 60], [14000, 65], [17000, 70]],
|
||||||
|
85: [[0, 50], [12500, 60], [14000, 65], [17000, 70]],
|
||||||
|
86: [[0, 50], [12500, 60], [14000, 65], [17000, 70]],
|
||||||
|
87: [[0, 50], [14000, 60]],
|
||||||
|
88: [[0, 50], [12500, 60], [14000, 65], [17000, 70]],
|
||||||
|
89: [[0, 50], [12500, 60], [14000, 65], [17000, 70]],
|
||||||
|
91: [[0, 40], [12500, 50], [14000, 60], [17000, 65]],
|
||||||
|
92: [[0, 40], [14000, 50]],
|
||||||
|
93: [[0, 40], [12500, 50], [14000, 60], [17000, 65]],
|
||||||
|
94: [[0, 40], [14000, 50]],
|
||||||
|
95: [[0, 40], [12500, 50], [14000, 60], [17000, 65]],
|
||||||
|
96: [[0, 40], [12500, 50], [14000, 60], [17000, 65]],
|
||||||
|
97: [[0, 40], [14000, 50]],
|
||||||
|
98: [[0, 50]],
|
||||||
|
99: [[0, 50]],
|
||||||
|
100: [[0, 50]],
|
||||||
|
101: [[0, 50]],
|
||||||
|
106: [[0, 50]]
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
key_mapL: [
|
key_mapL: [
|
||||||
|
|
||||||
@ -163,6 +287,5 @@
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
72
plot_calibrate.ipynb
Normal file
72
plot_calibrate.ipynb
Normal file
File diff suppressed because one or more lines are too long
150
plot_calibrate.py
Normal file
150
plot_calibrate.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import sys,os,json,types
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import matplotlib._color_data as mcd
|
||||||
|
from matplotlib.pyplot import figure
|
||||||
|
|
||||||
|
from rms_analysis import calibrate_recording_analysis
|
||||||
|
|
||||||
|
def plot_by_pitch( inDir, pitch=None ):
|
||||||
|
|
||||||
|
anlD = calibrate_recording_analysis( inDir )
|
||||||
|
jsonFn = os.path.join(inDir, "meas.json" )
|
||||||
|
audioFn = os.path.join(inDir, "audio.wav" )
|
||||||
|
|
||||||
|
with open(jsonFn,"r") as f:
|
||||||
|
r = json.load(f)
|
||||||
|
|
||||||
|
measD = r['measD']
|
||||||
|
cfg = types.SimpleNamespace(**r['cfg'])
|
||||||
|
|
||||||
|
axN = len(measD) if pitch is None else 1
|
||||||
|
fig,axL = plt.subplots(axN,1)
|
||||||
|
fig.set_size_inches(18.5, 10.5*axN)
|
||||||
|
|
||||||
|
|
||||||
|
# for each pitch
|
||||||
|
for axi,(midi_pitch,measL)in enumerate(measD.items()):
|
||||||
|
|
||||||
|
midi_pitch = int(midi_pitch)
|
||||||
|
|
||||||
|
if pitch is not None and pitch != midi_pitch:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if pitch is not None:
|
||||||
|
axi = 0
|
||||||
|
axL = [ axL ]
|
||||||
|
|
||||||
|
targetDbS = set()
|
||||||
|
hmPulseDbL = []
|
||||||
|
tdPulseDbL = []
|
||||||
|
anPulseDbL = []
|
||||||
|
|
||||||
|
# for each measurement on this pitch
|
||||||
|
for mi,d in enumerate(measL):
|
||||||
|
m = types.SimpleNamespace(**d)
|
||||||
|
|
||||||
|
# form a list of pulse/db measurements associated with this pitch
|
||||||
|
hmPulseDbL.append( (m.pulse_us,m.hm['db'],m.matchFl,m.hm['durMs'],m.skipMeasFl) )
|
||||||
|
tdPulseDbL.append( (m.pulse_us,m.td['db'],m.matchFl,m.td['durMs'],m.skipMeasFl) )
|
||||||
|
|
||||||
|
ar = next(ad for ad in anlD[midi_pitch] if ad['meas_idx']==mi )
|
||||||
|
anPulseDbL.append( (m.pulse_us,ar['db'],m.matchFl,m.hm['durMs'],m.skipMeasFl))
|
||||||
|
|
||||||
|
# get the unique set of targets
|
||||||
|
targetDbS.add(m.targetDb)
|
||||||
|
|
||||||
|
|
||||||
|
# sort measurements on pulse length
|
||||||
|
hmPulseDbL = sorted(hmPulseDbL,key=lambda x: x[0])
|
||||||
|
tdPulseDbL = sorted(tdPulseDbL,key=lambda x: x[0])
|
||||||
|
anPulseDbL = sorted(anPulseDbL,key=lambda x: x[0])
|
||||||
|
|
||||||
|
# plot the re-analysis
|
||||||
|
pulseL,dbL,matchFlL,_,_ = zip(*anPulseDbL)
|
||||||
|
axL[axi].plot( pulseL, dbL, label="post", marker='.' )
|
||||||
|
|
||||||
|
# plot harmonic measurements
|
||||||
|
pulseL,dbL,matchFlL,durMsL,skipFlL = zip(*hmPulseDbL)
|
||||||
|
axL[axi].plot( pulseL, dbL, label="harm", marker='.' )
|
||||||
|
|
||||||
|
# plot time-domain based measuremented
|
||||||
|
pulseL,dbL,matchFlL,_,_ = zip(*tdPulseDbL)
|
||||||
|
axL[axi].plot( pulseL, dbL, label="td", marker='.' )
|
||||||
|
|
||||||
|
|
||||||
|
# plot target boundaries
|
||||||
|
for targetDb in targetDbS:
|
||||||
|
lwr = targetDb * ((100.0 - cfg.tolDbPct)/100.0)
|
||||||
|
upr = targetDb * ((100.0 + cfg.tolDbPct)/100.0 )
|
||||||
|
|
||||||
|
axL[axi].axhline(targetDb)
|
||||||
|
axL[axi].axhline(lwr,color='lightgray')
|
||||||
|
axL[axi].axhline(upr,color='gray')
|
||||||
|
|
||||||
|
# plot match and 'too-short' markers
|
||||||
|
for i,matchFl in enumerate(matchFlL):
|
||||||
|
|
||||||
|
if durMsL[i] < cfg.minMeasDurMs:
|
||||||
|
axL[axi].plot( pulseL[i], dbL[i], marker='x', color='black', linestyle='None')
|
||||||
|
|
||||||
|
if skipFlL[i]:
|
||||||
|
axL[axi].plot( pulseL[i], dbL[i], marker='+', color='blue', linestyle='None')
|
||||||
|
|
||||||
|
if matchFl:
|
||||||
|
axL[axi].plot( pulseL[i], dbL[i], marker='.', color='red', linestyle='None')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
axL[axi].set_title("pitch:%i " % (midi_pitch))
|
||||||
|
|
||||||
|
plt.legend()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
def plot_all_notes( inDir ):
|
||||||
|
|
||||||
|
jsonFn = os.path.join(inDir, "meas.json" )
|
||||||
|
audioFn = os.path.join(inDir, "audio.wav" )
|
||||||
|
|
||||||
|
with open(jsonFn,"r") as f:
|
||||||
|
r = json.load(f)
|
||||||
|
|
||||||
|
measD = r['measD']
|
||||||
|
|
||||||
|
axN = 0
|
||||||
|
for midi_pitch,measL in measD.items():
|
||||||
|
axN += len(measL)
|
||||||
|
|
||||||
|
print(axN)
|
||||||
|
fig,axL = plt.subplots(axN,1)
|
||||||
|
fig.set_size_inches(18.5, 10.5*axN)
|
||||||
|
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
for midi_pitch,measL in measD.items():
|
||||||
|
for d in measL:
|
||||||
|
axL[i].plot(d['td']['rmsDbV'])
|
||||||
|
axL[i].plot(d['hm']['rmsDbV'])
|
||||||
|
|
||||||
|
axL[i].axvline(d['td']['pk_idx'],color='red')
|
||||||
|
axL[i].axvline(d['hm']['pk_idx'],color='green')
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
pitch = None
|
||||||
|
inDir = sys.argv[1]
|
||||||
|
if len(sys.argv) > 2:
|
||||||
|
pitch = int(sys.argv[2])
|
||||||
|
|
||||||
|
#plot_all_notes( inDir )
|
||||||
|
plot_by_pitch(inDir,pitch)
|
||||||
|
#calibrate_recording_analysis( inDir )
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
import os,sys,pickle
|
import os,sys,pickle,json
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
@ -279,9 +279,37 @@ def plot_quiet_note_db( cacheFn, yamlCfgFn, minDurMs=700 ):
|
|||||||
plt.show()
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def dump_hold_duty_pct( inDir ):
|
||||||
|
|
||||||
|
pitchL = []
|
||||||
|
folderL = os.listdir(inDir)
|
||||||
|
|
||||||
|
for folder in folderL:
|
||||||
|
|
||||||
|
midi_pitch = int(folder)
|
||||||
|
|
||||||
|
fn = os.path.join( inDir,folder,"0","seq.json")
|
||||||
|
|
||||||
|
if not os.path.isfile(fn):
|
||||||
|
print("No sequence file:%s" % (fn))
|
||||||
|
else:
|
||||||
|
with open(fn,"r") as f:
|
||||||
|
d = json.load(f)
|
||||||
|
|
||||||
|
if 'holdDutyPct' in d:
|
||||||
|
holdDutyPctL = [ [0,d['holdDutyPct']] ]
|
||||||
|
else:
|
||||||
|
holdDutyPctL = d['holdDutyPctL']
|
||||||
|
|
||||||
|
pitchL.append( {'pitch':midi_pitch, 'holdDutyPctL':holdDutyPctL} )
|
||||||
|
#print(midi_pitch, holdDutyPctL)
|
||||||
|
|
||||||
|
pitchL = sorted(pitchL, key=lambda x: x['pitch'])
|
||||||
|
|
||||||
|
for d in pitchL:
|
||||||
|
print("{",d['pitch'],":",d['holdDutyPctL'],"},")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
#inDir = sys.argv[1]
|
#inDir = sys.argv[1]
|
||||||
@ -297,4 +325,6 @@ if __name__ == "__main__":
|
|||||||
#get_all_note_durations("/home/kevin/temp/p_ac_3c",durFn)
|
#get_all_note_durations("/home/kevin/temp/p_ac_3c",durFn)
|
||||||
#plot_all_note_durations(durFn, np.arange(45,55),2,"p_ac.yml",800,20000)
|
#plot_all_note_durations(durFn, np.arange(45,55),2,"p_ac.yml",800,20000)
|
||||||
|
|
||||||
plot_quiet_note_db(durFn,"p_ac.yml")
|
#plot_quiet_note_db(durFn,"p_ac.yml")
|
||||||
|
|
||||||
|
dump_hold_duty_pct( "/home/kevin/temp/p_ac_3c" )
|
||||||
|
110
rms_analysis.py
110
rms_analysis.py
@ -1,4 +1,4 @@
|
|||||||
import os,types,json,pickle
|
import os,types,json,pickle,types
|
||||||
from scipy.io import wavfile
|
from scipy.io import wavfile
|
||||||
from scipy.signal import stft
|
from scipy.signal import stft
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -27,8 +27,9 @@ def calc_harm_bins( srate, binHz, midiPitch, harmN ):
|
|||||||
return fund_l_binL, fund_m_binL, fund_u_binL
|
return fund_l_binL, fund_m_binL, fund_u_binL
|
||||||
|
|
||||||
def rms_to_db( xV, rms_srate, refWndMs ):
|
def rms_to_db( xV, rms_srate, refWndMs ):
|
||||||
dbWndN = int(round(refWndMs * rms_srate / 1000.0))
|
#dbWndN = int(round(refWndMs * rms_srate / 1000.0))
|
||||||
dbRef = ref = np.mean(xV[0:dbWndN])
|
#dbRef = ref = np.mean(xV[0:dbWndN])
|
||||||
|
dbRef = refWndMs ######################################################### HACK HACK HACK HACK HACK
|
||||||
rmsDbV = 20.0 * np.log10( xV / dbRef )
|
rmsDbV = 20.0 * np.log10( xV / dbRef )
|
||||||
|
|
||||||
return rmsDbV
|
return rmsDbV
|
||||||
@ -213,9 +214,6 @@ def note_stats( r, decay_pct=50.0, extraDurSearchMs=500 ):
|
|||||||
bi = pkSmpIdx
|
bi = pkSmpIdx
|
||||||
ei = pkSmpIdx + int(round(durMs * srate / 1000.0))
|
ei = pkSmpIdx + int(round(durMs * srate / 1000.0))
|
||||||
|
|
||||||
#bi = begSmpIdx
|
|
||||||
#ei = endSmpIdx
|
|
||||||
|
|
||||||
qualityCoeff = np.sum(r.rmsDbV[bi:ei]) + np.sum(r.tdRmsDbV[bi:ei])
|
qualityCoeff = np.sum(r.rmsDbV[bi:ei]) + np.sum(r.tdRmsDbV[bi:ei])
|
||||||
if qualityCoeff > qmax:
|
if qualityCoeff > qmax:
|
||||||
qmax = qualityCoeff
|
qmax = qualityCoeff
|
||||||
@ -259,6 +257,106 @@ def key_info_dictionary( keyMapL=None, yamlCfgFn=None):
|
|||||||
|
|
||||||
return kmD
|
return kmD
|
||||||
|
|
||||||
|
def rms_analyze_one_rt_note( sigV, srate, begMs, endMs, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=500, harmCandN=5, harmN=3, durDecayPct=40 ):
|
||||||
|
|
||||||
|
sigV = np.squeeze(sigV)
|
||||||
|
|
||||||
|
# HACK HACK HACK HACK
|
||||||
|
dbRefWndMs = 0.002 # HACK HACK HACK HACK
|
||||||
|
# HACK HACK HACK HACK
|
||||||
|
td_rmsDbV, td_srate = audio_rms( srate, sigV, rmsWndMs, rmsHopMs, dbRefWndMs )
|
||||||
|
|
||||||
|
begSmpIdx = int(round(begMs * td_srate/1000))
|
||||||
|
endSmpIdx = int(round(endMs * td_srate/1000))
|
||||||
|
td_pk_idx = begSmpIdx + np.argmax(td_rmsDbV[begSmpIdx:endSmpIdx])
|
||||||
|
|
||||||
|
td_durMs = measure_duration_ms( td_rmsDbV, td_srate, td_pk_idx, len(sigV)-1, durDecayPct )
|
||||||
|
|
||||||
|
# HACK HACK HACK HACK
|
||||||
|
dbRefWndMs = 0.01 # HACK HACK HACK HACK
|
||||||
|
# HACK HACK HACK HACK
|
||||||
|
|
||||||
|
hm_rmsDbV, hm_srate, binHz = audio_harm_rms( srate, sigV, rmsWndMs, rmsHopMs, dbRefWndMs, midi_pitch, harmCandN, harmN )
|
||||||
|
|
||||||
|
begSmpIdx = int(round(begMs * hm_srate/1000))
|
||||||
|
endSmpIdx = int(round(endMs * hm_srate/1000))
|
||||||
|
hm_pk_idx = begSmpIdx + np.argmax(hm_rmsDbV[begSmpIdx:endSmpIdx])
|
||||||
|
|
||||||
|
hm_durMs = measure_duration_ms( hm_rmsDbV, hm_srate, hm_pk_idx, len(sigV)-1, durDecayPct )
|
||||||
|
|
||||||
|
tdD = { "rmsDbV":td_rmsDbV.tolist(), "srate":td_srate, "pk_idx":int(td_pk_idx), "db":float(td_rmsDbV[td_pk_idx]), "durMs":td_durMs }
|
||||||
|
hmD = { "rmsDbV":hm_rmsDbV.tolist(), "srate":hm_srate, "pk_idx":int(hm_pk_idx), "db":float(hm_rmsDbV[hm_pk_idx]), "durMs":hm_durMs }
|
||||||
|
|
||||||
|
return { "td":tdD, "hm":hmD }
|
||||||
|
|
||||||
|
def calibrate_rms( sigV, srate, beg_ms, end_ms ):
|
||||||
|
|
||||||
|
bi = int(round(beg_ms * srate / 1000))
|
||||||
|
ei = int(round(end_ms * srate / 1000))
|
||||||
|
rms = np.sqrt( np.mean( sigV[bi:ei] * sigV[bi:ei] ))
|
||||||
|
|
||||||
|
return 20.0*np.log10( rms / 0.002 )
|
||||||
|
|
||||||
|
|
||||||
|
def calibrate_recording_analysis( inDir ):
|
||||||
|
|
||||||
|
jsonFn = os.path.join(inDir, "meas.json" )
|
||||||
|
audioFn = os.path.join(inDir, "audio.wav" )
|
||||||
|
|
||||||
|
with open(jsonFn,"r") as f:
|
||||||
|
r = json.load(f)
|
||||||
|
|
||||||
|
measD = r['measD']
|
||||||
|
cfg = types.SimpleNamespace(**r['cfg'])
|
||||||
|
annL = r['annoteL']
|
||||||
|
anlD = {}
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
for midi_pitch,measL in measD.items():
|
||||||
|
n += len(measL)
|
||||||
|
anlD[int(midi_pitch)] = []
|
||||||
|
|
||||||
|
srate, signalM = wavfile.read(audioFn)
|
||||||
|
sigV = signalM / float(0x7fff)
|
||||||
|
|
||||||
|
anlr = types.SimpleNamespace(**cfg.analysisD)
|
||||||
|
|
||||||
|
# HACK HACK HACK HACK
|
||||||
|
dbRefWndMs = 0.002 # HACK HACK HACK HACK
|
||||||
|
# HACK HACK HACK HACK
|
||||||
|
|
||||||
|
tdRmsDbV, td_srate = audio_rms( srate, sigV, anlr.rmsWndMs, anlr.rmsHopMs, dbRefWndMs )
|
||||||
|
|
||||||
|
# for each measured pitch
|
||||||
|
for midi_pitch,measL in measD.items():
|
||||||
|
|
||||||
|
# for each measured note at this pitch
|
||||||
|
for mi,d in enumerate(measL):
|
||||||
|
|
||||||
|
mr = types.SimpleNamespace(**d)
|
||||||
|
|
||||||
|
# locate the associated annotation reocrd
|
||||||
|
for annD in annL:
|
||||||
|
ar = types.SimpleNamespace(**annD)
|
||||||
|
|
||||||
|
if ar.midi_pitch == mr.midi_pitch and ar.beg_ms==mr.beg_ms and ar.end_ms==mr.end_ms:
|
||||||
|
assert( ar.pulse_us == mr.pulse_us )
|
||||||
|
|
||||||
|
bi = int(round(ar.beg_ms * td_srate / 1000))
|
||||||
|
ei = int(round(ar.end_ms * td_srate / 1000))
|
||||||
|
db = np.mean(tdRmsDbV[bi:ei])
|
||||||
|
|
||||||
|
db = calibrate_rms( sigV, srate, ar.beg_ms, ar.end_ms )
|
||||||
|
|
||||||
|
anlD[int(midi_pitch)].append({ 'pulse_us':ar.pulse_us, 'db':db, 'meas_idx':mi })
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
return anlD
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=500, harmCandN=5, harmN=3, durDecayPct=40 ):
|
def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=500, harmCandN=5, harmN=3, durDecayPct=40 ):
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user