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 AudioDevice import AudioDevice
|
||||
from MidiDevice import MidiDevice
|
||||
from result import Result
|
||||
from common import parse_yaml_cfg
|
||||
from plot_seq import form_resample_pulse_time_list
|
||||
from plot_seq import form_final_pulse_list
|
||||
from rt_note_analysis import RT_Analyzer
|
||||
from keyboard import Keyboard
|
||||
from calibrate import Calibrate
|
||||
|
||||
class AttackPulseSeq:
|
||||
""" Sequence a fixed chord over a list of attack pulse lengths."""
|
||||
@ -274,13 +276,15 @@ class App:
|
||||
self.cfg = None
|
||||
self.audioDev = None
|
||||
self.api = None
|
||||
self.calibrate = None
|
||||
self.cal_keys = None
|
||||
self.keyboard = None
|
||||
self.calibrate = None
|
||||
|
||||
def setup( self, cfg ):
|
||||
self.cfg = cfg
|
||||
|
||||
self.audioDev = AudioDevice()
|
||||
self.midiDev = MidiDevice()
|
||||
|
||||
#
|
||||
# TODO: unify the result error handling
|
||||
@ -292,20 +296,31 @@ class App:
|
||||
if not res:
|
||||
self.audio_dev_list(0)
|
||||
else:
|
||||
self.api = Picadae( key_mapL=cfg.key_mapL)
|
||||
|
||||
# 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)
|
||||
if hasattr(cfg,'midi'):
|
||||
res = self.midiDev.setup(**cfg.midi)
|
||||
|
||||
# did the serial port sync fail?
|
||||
if not api_res:
|
||||
res.set_error("Serial port sync failed.")
|
||||
if not res:
|
||||
self.midi_dev_list(0)
|
||||
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
|
||||
|
||||
@ -313,24 +328,40 @@ class App:
|
||||
|
||||
self.audioDev.tick(ms)
|
||||
|
||||
if self.calibrate:
|
||||
self.calibrate.tick(ms)
|
||||
if self.cal_keys:
|
||||
self.cal_keys.tick(ms)
|
||||
|
||||
if self.keyboard:
|
||||
self.keyboard.tick(ms)
|
||||
|
||||
if self.calibrate:
|
||||
self.calibrate.tick(ms)
|
||||
|
||||
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']))
|
||||
|
||||
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 ):
|
||||
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 ):
|
||||
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 ):
|
||||
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 ):
|
||||
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 ):
|
||||
self.calibrate.stop(ms)
|
||||
self.cal_keys.stop(ms)
|
||||
self.keyboard.stop(ms)
|
||||
self.calibrate.stop(ms)
|
||||
|
||||
def quit( self, ms ):
|
||||
if self.api:
|
||||
@ -424,7 +462,7 @@ def app_event_loop_func( pipe, cfg ):
|
||||
|
||||
|
||||
# give some time to the system
|
||||
time.sleep(0.1)
|
||||
time.sleep(0.05)
|
||||
|
||||
# calc the tick() time stamp
|
||||
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"},
|
||||
'?':{ "func":"_help", "minN":0, "maxN":0, "help":"Print usage text."},
|
||||
'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. "},
|
||||
'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"},
|
||||
'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"},
|
||||
|
127
p_ac.yml
127
p_ac.yml
@ -6,8 +6,17 @@
|
||||
audio: {
|
||||
inPortLabel: "5 USB Audio CODEC:", #"HDA Intel PCH: CS4208", # "5 USB Audio CODEC:", #"5 USB Sound Device",
|
||||
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
|
||||
serial_dev: "/dev/ttyACM0",
|
||||
@ -62,6 +71,121 @@
|
||||
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: [
|
||||
|
||||
{ index: 0, board: 1, ch: 1, type: 'wB', midi: 21, class: 'A' },
|
||||
@ -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 matplotlib.pyplot as plt
|
||||
@ -279,7 +279,35 @@ def plot_quiet_note_db( cacheFn, yamlCfgFn, minDurMs=700 ):
|
||||
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__":
|
||||
@ -297,4 +325,6 @@ if __name__ == "__main__":
|
||||
#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_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.signal import stft
|
||||
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
|
||||
|
||||
def rms_to_db( xV, rms_srate, refWndMs ):
|
||||
dbWndN = int(round(refWndMs * rms_srate / 1000.0))
|
||||
dbRef = ref = np.mean(xV[0:dbWndN])
|
||||
#dbWndN = int(round(refWndMs * rms_srate / 1000.0))
|
||||
#dbRef = ref = np.mean(xV[0:dbWndN])
|
||||
dbRef = refWndMs ######################################################### HACK HACK HACK HACK HACK
|
||||
rmsDbV = 20.0 * np.log10( xV / dbRef )
|
||||
|
||||
return rmsDbV
|
||||
@ -213,9 +214,6 @@ def note_stats( r, decay_pct=50.0, extraDurSearchMs=500 ):
|
||||
bi = pkSmpIdx
|
||||
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])
|
||||
if qualityCoeff > qmax:
|
||||
qmax = qualityCoeff
|
||||
@ -259,6 +257,106 @@ def key_info_dictionary( keyMapL=None, yamlCfgFn=None):
|
||||
|
||||
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 ):
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user