AudioDevice.py : fixed linear_buffer()

p_ac.py : changed dutyCyclePct to a list, added real-timen note analysis and multi-pitch tests.
Added keyboard.py, rt_note_analysis.py, plot_note_analysis.py, plot_all_note_durations.ipynb.
This commit is contained in:
kpl 2019-11-18 11:44:47 -05:00
parent 919ecadf8d
commit 26b997811a
10 changed files with 969 additions and 67 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.pyc
.ipynb_checkpoints

View File

@ -191,14 +191,16 @@ class AudioDevice(object):
def linear_buffer( self ): def linear_buffer( self ):
smpN = self.buffer_sample_count() r = self.buffer_sample_count()
bV = np.zeros( (smpN,self.ch_cnt) ) if r:
smpN = r.value
bV = np.zeros( (smpN,self.ch_cnt) )
bi = 0 bi = 0
for i in range(len(self.bufL)): for i in range(len(self.bufL)):
bufSmpN = self.bufIdx if i == len(self.bufL)-1 else self.bufL[i].shape[0] bufSmpN = self.bufIdx if i == len(self.bufL)-1 else self.bufL[i].shape[0]
bV[ bi:bi+bufSmpN, ] = self.bufL[i][0:bufSmpN] bV[ bi:bi+bufSmpN, ] = self.bufL[i][0:bufSmpN]
bi += bufSmpN bi += bufSmpN
return Result(bV) return Result(bV)

209
keyboard.py Normal file
View File

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

82
p_ac.py
View File

@ -10,11 +10,14 @@ 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 keyboard import Keyboard
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."""
def __init__(self, audio, api, noteDurMs=1000, pauseDurMs=1000, holdDutyPctL=[(0,50)] ): def __init__(self, cfg, audio, api, noteDurMs=1000, pauseDurMs=1000 ):
self.cfg = cfg
self.audio = audio self.audio = audio
self.api = api self.api = api
self.outDir = None # directory to write audio file and results self.outDir = None # directory to write audio file and results
@ -22,7 +25,7 @@ class AttackPulseSeq:
self.pulseUsL = [] # one onset pulse length in microseconds per sequence element self.pulseUsL = [] # one onset pulse length in microseconds per sequence element
self.noteDurMs = noteDurMs # duration of each chord in milliseconds self.noteDurMs = noteDurMs # duration of each chord in milliseconds
self.pauseDurMs = pauseDurMs # duration between end of previous note and start of next self.pauseDurMs = pauseDurMs # duration between end of previous note and start of next
self.holdDutyPctL= holdDutyPctL # hold voltage duty cycle table [ (minPulseSeqUsec,dutyCyclePct) ] self.holdDutyPctL= None # hold voltage duty cycle table [ (minPulseSeqUsec,dutyCyclePct) ]
self.pulse_idx = 0 # Index of next pulse self.pulse_idx = 0 # Index of next pulse
self.state = None # 'note_on','note_off' self.state = None # 'note_on','note_off'
@ -31,12 +34,13 @@ class AttackPulseSeq:
self.eventTimeL = [] # Onset/offset time of each note [ [onset_ms,offset_ms] ] (used to locate the note in the audio file) self.eventTimeL = [] # Onset/offset time of each note [ [onset_ms,offset_ms] ] (used to locate the note in the audio file)
self.beginMs = 0 self.beginMs = 0
self.playOnlyFl = False self.playOnlyFl = False
self.rtAnalyzer = RT_Analyzer()
def start( self, ms, outDir, pitchL, pulseUsL, playOnlyFl=False ): def start( self, ms, outDir, pitchL, pulseUsL, holdDutyPctL, playOnlyFl=False ):
self.outDir = outDir # directory to write audio file and results self.outDir = outDir # directory to write audio file and results
self.pitchL = pitchL # chord to play self.pitchL = pitchL # chord to play
self.pulseUsL = pulseUsL # one onset pulse length in microseconds per sequence element self.pulseUsL = pulseUsL # one onset pulse length in microseconds per sequence element
self.holdDutyPctL = holdDutyPctL
self.pulse_idx = 0 self.pulse_idx = 0
self.state = 'note_on' self.state = 'note_on'
self.prevHoldDutyPct = None self.prevHoldDutyPct = None
@ -49,15 +53,16 @@ class AttackPulseSeq:
# self.api.set_pwm_duty( pitch, self.holdDutyPct ) # self.api.set_pwm_duty( pitch, self.holdDutyPct )
# print("set PWM:%i"%(self.holdDutyPct)) # print("set PWM:%i"%(self.holdDutyPct))
if not playOnlyFl: # kpl if not playOnlyFl:
self.audio.record_enable(True) # start recording audio self.audio.record_enable(True) # start recording audio
self.tick(ms) # play the first note self.tick(ms) # play the first note
def stop(self, ms): def stop(self, ms):
self._send_note_off() # be sure that all notes are actually turn-off self._send_note_off() # be sure that all notes are actually turn-off
if not self.playOnlyFl: # kpl if not self.playOnlyFl:
self.audio.record_enable(False) # stop recording audio self.audio.record_enable(False) # stop recording audio
self._disable() # disable this sequencer self._disable() # disable this sequencer
@ -69,8 +74,6 @@ class AttackPulseSeq:
def tick(self, ms): def tick(self, ms):
self.audio.tick(ms)
# if next event time has arrived # if next event time has arrived
if self.is_enabled() and ms >= self.next_ms: if self.is_enabled() and ms >= self.next_ms:
@ -120,12 +123,17 @@ class AttackPulseSeq:
pulse_usec = int(self.pulseUsL[ self.pulse_idx ]) pulse_usec = int(self.pulseUsL[ self.pulse_idx ])
self._set_duty_cycle( pitch, pulse_usec ) self._set_duty_cycle( pitch, pulse_usec )
self.api.note_on_us( pitch, pulse_usec ) self.api.note_on_us( pitch, pulse_usec )
print("note-on:",pitch,self.pulse_idx) print("note-on:",pitch, self.pulse_idx, pulse_usec)
def _note_off( self, ms ): def _note_off( self, ms ):
self.eventTimeL[ self.pulse_idx ][1] = self.audio.buffer_sample_ms().value self.eventTimeL[ self.pulse_idx ][1] = self.audio.buffer_sample_ms().value
self.next_ms = ms + self.pauseDurMs self.next_ms = ms + self.pauseDurMs
self.state = 'note_on' self.state = 'note_on'
if self.playOnlyFl:
begTimeMs = self.eventTimeL[ self.pulse_idx ][0]
endTimeMs = self.eventTimeL[ self.pulse_idx ][1]
self.rtAnalyzer.analyze_note( self.audio, self.pitchL[0], begTimeMs, endTimeMs, self.cfg.analysisArgs['rmsAnalysisArgs'] )
self._send_note_off() self._send_note_off()
@ -133,7 +141,7 @@ class AttackPulseSeq:
def _send_note_off( self ): def _send_note_off( self ):
for pitch in self.pitchL: for pitch in self.pitchL:
self.api.note_off( pitch ) self.api.note_off( pitch )
print("note-off:",pitch,self.pulse_idx) #print("note-off:",pitch,self.pulse_idx)
def _disable(self): def _disable(self):
self.state = None self.state = None
@ -167,7 +175,7 @@ class AttackPulseSeq:
class CalibrateKeys: class CalibrateKeys:
def __init__(self, cfg, audioDev, api): def __init__(self, cfg, audioDev, api):
self.cfg = cfg self.cfg = cfg
self.seq = AttackPulseSeq( audioDev, api, noteDurMs=cfg.noteDurMs, pauseDurMs=cfg.pauseDurMs, holdDutyPctL=cfg.holdDutyPctL ) self.seq = AttackPulseSeq( cfg, audioDev, api, noteDurMs=cfg.noteDurMs, pauseDurMs=cfg.pauseDurMs )
self.pulseUsL = None self.pulseUsL = None
self.chordL = None self.chordL = None
@ -233,8 +241,10 @@ class CalibrateKeys:
if outDir_id != 0: if outDir_id != 0:
self.pulseUsL,_,_ = form_resample_pulse_time_list( outDir, self.cfg.analysisArgs ) self.pulseUsL,_,_ = form_resample_pulse_time_list( outDir, self.cfg.analysisArgs )
holdDutyPctL = self.cfg.holdDutyPctL
if playOnlyFl: if playOnlyFl:
self.pulseUsL,_ = form_final_pulse_list( outDir, pitchL[0], self.cfg.analysisArgs, take_id=None ) self.pulseUsL,_,holdDutyPctL = form_final_pulse_list( outDir, pitchL[0], self.cfg.analysisArgs, take_id=None )
noteN = cfg.analysisArgs['auditionNoteN'] noteN = cfg.analysisArgs['auditionNoteN']
self.pulseUsL = [ self.pulseUsL[ int(round(i*126.0/(noteN-1)))] for i in range(noteN) ] self.pulseUsL = [ self.pulseUsL[ int(round(i*126.0/(noteN-1)))] for i in range(noteN) ]
@ -246,7 +256,7 @@ class CalibrateKeys:
os.mkdir(outDir) os.mkdir(outDir)
# start the sequencer # start the sequencer
self.seq.start( ms, outDir, pitchL, self.pulseUsL, playOnlyFl ) self.seq.start( ms, outDir, pitchL, self.pulseUsL, holdDutyPctL, playOnlyFl )
def _calc_next_out_dir_id( self, outDir ): def _calc_next_out_dir_id( self, outDir ):
@ -265,6 +275,7 @@ class App:
self.audioDev = None self.audioDev = None
self.api = None self.api = None
self.calibrate = None self.calibrate = None
self.keyboard = None
def setup( self, cfg ): def setup( self, cfg ):
self.cfg = cfg self.cfg = cfg
@ -294,12 +305,18 @@ class App:
self.calibrate = CalibrateKeys( cfg, self.audioDev, self.api ) self.calibrate = CalibrateKeys( cfg, self.audioDev, self.api )
self.keyboard = Keyboard( cfg, self.audioDev, self.api )
return res return res
def tick( self, ms ): def tick( self, ms ):
self.audioDev.tick(ms)
if self.calibrate: if self.calibrate:
self.calibrate.tick(ms) self.calibrate.tick(ms)
if self.keyboard:
self.keyboard.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 )
@ -315,9 +332,24 @@ class App:
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.calibrate.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)]
self.keyboard.start( ms, pitchL, argL[2], None )
def keyboard_repeat_pulse_idx( self, ms, argL ):
self.keyboard.repeat( ms, argL[0], None )
def keyboard_start_target_db( self, ms, argL ):
pitchL = [ pitch for pitch in range(argL[0], argL[1]+1)]
self.keyboard.start( ms, pitchL, None, argL[2] )
def keyboard_repeat_target_db( self, ms, argL ):
self.keyboard.repeat( ms, None, argL[0] )
def calibrate_keys_stop( self, ms ): def calibrate_keys_stop( self, ms ):
self.calibrate.stop(ms) self.calibrate.stop(ms)
self.keyboard.stop(ms)
def quit( self, ms ): def quit( self, ms ):
if self.api: if self.api:
self.api.close() self.api.close()
@ -435,12 +467,16 @@ class Shell:
def __init__( self, cfg ): def __init__( self, cfg ):
self.appProc = None self.appProc = None
self.parseD = { self.parseD = {
'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."},
'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. "},
'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"},
'r':{ "func":"keyboard_repeat_pulse_idx", "minN":1, "maxN":1, "help":"Repeat pulse index across keyboard with new pulse_idx"},
'K':{ "func":"keyboard_start_target_db", "minN":3, "maxN":3, "help":"Play db across keyboard"},
'R':{ "func":"keyboard_repeat_target_db", "minN":1, "maxN":1, "help":"Repeat db across keyboard with new pulse_idx"},
} }
def _help( self, _=None ): def _help( self, _=None ):
@ -515,10 +551,8 @@ class Shell:
# tokenize the command # tokenize the command
tokL = s.split(' ') tokL = s.split(' ')
# execute the command # execute the command
result = self._exec_cmd( tokL ) result = self._exec_cmd( tokL )
# if this is the 'quit' command # if this is the 'quit' command
if tokL[0] == 'q': if tokL[0] == 'q':

View File

@ -21,7 +21,7 @@
outDir: "~/temp/p_ac_3c", outDir: "~/temp/p_ac_3c",
noteDurMs: 1000, noteDurMs: 1000,
pauseDurMs: 1000, pauseDurMs: 1000,
holdDutyPctL: [ [0,50], [17000,65] ], holdDutyPctL: [ [0,50], [22000,55] ],
full_pulse0L: [ 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 6500, 7000, 8000, 9000, 10000, 12000, 14000, 18000, 22000, 26000, 30000, 34000, 40000], full_pulse0L: [ 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 6500, 7000, 8000, 9000, 10000, 12000, 14000, 18000, 22000, 26000, 30000, 34000, 40000],
full_pulse1L: [ 10000, 11000, 12000, 13000, 14000, 15000, 16000, 17000, 18000, 20000, 22000, 24000, 26000, 30000, 32000, 34000, 36000, 40000], full_pulse1L: [ 10000, 11000, 12000, 13000, 14000, 15000, 16000, 17000, 18000, 20000, 22000, 24000, 26000, 30000, 32000, 34000, 36000, 40000],
@ -35,9 +35,9 @@
full_pulse6L: [ 12000, 12125, 12250, 12375, 12500, 12625, 12750, 12875, 13000, 13125, 13250, 13375, 13500, 13625, 13750, 13875, 14000, 14125, 14250, 14375, 14500, 14625, 14750, 14875, 15000, 15250, 15375, 15500, 15750, 16000, 16250, 16500, 16750, 17000, 17250, 17500, 17750, 18000, 18250, 18500, 18750, 19000, 19500, 20000, 20500, 21000, 21500, 22000, 22500, 23000, 23500, 24000, 24500, 25000, 25500, 26000, 26500, 27000, 27500, 28000, 28500, 29000, 30000, 31000, 32000, 33000, 34000, 35000, 36000, 37000, 38000, 39000, 40000 ], full_pulse6L: [ 12000, 12125, 12250, 12375, 12500, 12625, 12750, 12875, 13000, 13125, 13250, 13375, 13500, 13625, 13750, 13875, 14000, 14125, 14250, 14375, 14500, 14625, 14750, 14875, 15000, 15250, 15375, 15500, 15750, 16000, 16250, 16500, 16750, 17000, 17250, 17500, 17750, 18000, 18250, 18500, 18750, 19000, 19500, 20000, 20500, 21000, 21500, 22000, 22500, 23000, 23500, 24000, 24500, 25000, 25500, 26000, 26500, 27000, 27500, 28000, 28500, 29000, 30000, 31000, 32000, 33000, 34000, 35000, 36000, 37000, 38000, 39000, 40000 ],
full_pulse7L: [ 11000, 11125, 11250, 11375, 11500, 11625, 11750, 11875, 12000, 12125, 12250, 12375, 12500, 12625, 12750, 12875, 13000, 13125, 13250, 13375, 13500, 13625, 13750, 13875, 14000, 14125, 14250, 14375, 14500, 14625, 14750, 14875, 15000, 15250, 15375, 15500, 15750, 16000, 16250, 16500, 16750, 17000, 17250, 17500, 17750, 18000, 18250, 18500, 18750, 19000, 19500, 20000, 20500, 21000, 21500, 22000, 22500, 23000, 23500, 24000, 24500, 25000, 25500, 26000, 26500, 27000, 27500, 28000, 28500, 29000, 30000, 31000, 32000, 33000, 34000, 35000, 36000, 37000, 38000, 39000, 40000 ], full_pulseL: [ 11000, 11125, 11250, 11375, 11500, 11625, 11750, 11875, 12000, 12125, 12250, 12375, 12500, 12625, 12750, 12875, 13000, 13125, 13250, 13375, 13500, 13625, 13750, 13875, 14000, 14125, 14250, 14375, 14500, 14625, 14750, 14875, 15000, 15250, 15375, 15500, 15750, 16000, 16250, 16500, 16750, 17000, 17250, 17500, 17750, 18000, 18250, 18500, 18750, 19000, 19500, 20000, 20500, 21000, 21500, 22000, 22500, 23000, 23500, 24000, 24500, 25000, 25500, 26000, 26500, 27000, 27500, 28000, 28500, 29000, 30000, 31000, 32000, 33000, 34000, 35000, 36000, 37000, 38000, 39000, 40000 ],
full_pulseL: [ 10000, 10050, 10100, 10150, 10200, 10250, 10300, 10350, 10400, 10450, 10500, 10550, 10600, 10650, 10700, 10750, 10800, 10850, 10900, 10950, 11000, 11125, 11250, 11375, 11500, 11625, 11750, 11875, 12000, 12125, 12250, 12375, 12500, 12625, 12750, 12875, 13000, 13125, 13250, 13375, 13500, 13625, 13750, 13875, 14000, 14125, 14250, 14375, 14500, 14625, 14750, 14875, 15000, 15250, 15375, 15500, 15750, 16000, 16250, 16500, 16750, 17000, 17250, 17500, 17750, 18000, 18250, 18500, 18750, 19000, 19500, 20000, 20500, 21000, 21500, 22000, 22500, 23000, 23500, 24000, 24500, 25000, 25500, 26000, 26500, 27000, 27500, 28000, 28500, 29000, 30000, 31000, 32000, 33000, 34000, 35000, 36000, 37000, 38000, 39000, 40000 ], full_pulse7L: [ 10000, 10050, 10100, 10150, 10200, 10250, 10300, 10350, 10400, 10450, 10500, 10550, 10600, 10650, 10700, 10750, 10800, 10850, 10900, 10950, 11000, 11125, 11250, 11375, 11500, 11625, 11750, 11875, 12000, 12125, 12250, 12375, 12500, 12625, 12750, 12875, 13000, 13125, 13250, 13375, 13500, 13625, 13750, 13875, 14000, 14125, 14250, 14375, 14500, 14625, 14750, 14875, 15000, 15250, 15375, 15500, 15750, 16000, 16250, 16500, 16750, 17000, 17250, 17500, 17750, 18000, 18250, 18500, 18750, 19000, 19500, 20000, 20500, 21000, 21500, 22000, 22500, 23000, 23500, 24000, 24500, 25000, 25500, 26000, 26500, 27000, 27500, 28000, 28500, 29000, 30000, 31000, 32000, 33000, 34000, 35000, 36000, 37000, 38000, 39000, 40000 ],
full_pulse9L: [ 8750, 8800, 8850, 8900, 8950, 9000, 9050, 9100, 9150, 9200, 9250, 9300, 9350, 9400, 9450,9500, 9550, 9600, 9650, 9700, 9750, 9800, 9850, 9900, 9950, 10000, 10050, 10100, 10150, 10200, 10250, 10300, 10350, 10400, 10450, 10500, 10550, 10600, 10650, 10700, 10750, 10800, 10850, 10900, 10950, 11000, 11125, 11250, 11375, 11500, 11625, 11750, 11875, 12000, 12125, 12250, 12375, 12500, 12625, 12750, 12875, 13000, 13125, 13250, 13375, 13500, 13625, 13750, 13875, 14000, 14125, 14250, 14375, 14500, 14625, 14750, 14875, 15000, 15250, 15375, 15500, 15750, 16000, 16250, 16500, 16750, 17000, 17250, 17500, 17750, 18000, 18250, 18500, 18750, 19000, 19500, 20000, 20500, 21000, 21500, 22000, 22500, 23000, 23500, 24000, 24500, 25000, 25500, 26000, 26500, 27000, 27500, 28000, 28500, 29000, 30000, 31000, 32000, 33000, 34000, 35000, 36000, 37000, 38000, 39000, 40000 ], full_pulse9L: [ 8750, 8800, 8850, 8900, 8950, 9000, 9050, 9100, 9150, 9200, 9250, 9300, 9350, 9400, 9450,9500, 9550, 9600, 9650, 9700, 9750, 9800, 9850, 9900, 9950, 10000, 10050, 10100, 10150, 10200, 10250, 10300, 10350, 10400, 10450, 10500, 10550, 10600, 10650, 10700, 10750, 10800, 10850, 10900, 10950, 11000, 11125, 11250, 11375, 11500, 11625, 11750, 11875, 12000, 12125, 12250, 12375, 12500, 12625, 12750, 12875, 13000, 13125, 13250, 13375, 13500, 13625, 13750, 13875, 14000, 14125, 14250, 14375, 14500, 14625, 14750, 14875, 15000, 15250, 15375, 15500, 15750, 16000, 16250, 16500, 16750, 17000, 17250, 17500, 17750, 18000, 18250, 18500, 18750, 19000, 19500, 20000, 20500, 21000, 21500, 22000, 22500, 23000, 23500, 24000, 24500, 25000, 25500, 26000, 26500, 27000, 27500, 28000, 28500, 29000, 30000, 31000, 32000, 33000, 34000, 35000, 36000, 37000, 38000, 39000, 40000 ],
@ -56,7 +56,10 @@
maxDeltaDb: 1.5, # maximum db change between volume samples (changes greater than this will trigger resampling) maxDeltaDb: 1.5, # maximum db change between volume samples (changes greater than this will trigger resampling)
samplesPerDb: 4, # count of samples per dB to resample ranges whose range is less than maxDeltaDb samplesPerDb: 4, # count of samples per dB to resample ranges whose range is less than maxDeltaDb
minSampleDistUs: 50, # minimum distance between sample points in microseconds minSampleDistUs: 50, # minimum distance between sample points in microseconds
auditionNoteN: 19 # count of notes to play for audition auditionNoteN: 19, # count of notes to play for audition
finalPulseListCacheFn: "/home/kevin/temp/final_pulse_list_cache.pickle",
rmsAnalysisCacheFn: "/home/kevin/temp/rms_analysis_cache.pickle"
}, },
key_mapL: [ key_mapL: [

File diff suppressed because one or more lines are too long

300
plot_note_analysis.py Normal file
View File

@ -0,0 +1,300 @@
import os,sys,pickle
import numpy as np
import matplotlib.pyplot as plt
import matplotlib._color_data as mcd
from matplotlib.pyplot import figure
from rms_analysis import rms_analysis_main
from rms_analysis import note_stats
from rms_analysis import key_info_dictionary
from rms_analysis import select_first_stable_note_by_delta_db
from rms_analysis import select_first_stable_note_by_dur
def do_plot(r, statsL ):
fig,ax = plt.subplots()
x = [ i / r.rms_srate for i in range(len(r.tdRmsDbV)) ]
ax.plot( x,r.tdRmsDbV, color="blue" )
x = [ i / r.rms_srate for i in range(len(r.rmsDbV)) ]
ax.plot( x,r.rmsDbV, color="green")
ymx = np.max(r.tdRmsDbV)
ymn = np.min(r.tdRmsDbV)
for r in statsL:
x = r.pkSmpSec
ax.axvline(x,ymax=ymx,ymin=ymn)
ax.text(x,r.pkDb+1,"%i ms" % r.durMs)
ax.text(x,r.pkDb+2,"%4.1f dB" % r.pkDb)
ax.text(x,r.pkDb+3,"%i us" % r.pulse_us)
if hasattr(r,"MIN"):
ax.plot(x,r.pkDb,marker="*",color="red")
plt.show()
def select_min_note( rr, statsL, minDurMs=600, minDb=8, contextSecs=10 ):
sel_note_r = None
for r in statsL:
if r.pkDb > minDb and r.durMs > minDurMs:
sel_note_r = r
break
#print(rr.tdRmsDbV.shape,rr.rmsDbV.shape)
if sel_note_r is None:
print("ERROR: No min note found.")
else:
#print(sel_note_r)
bi = max(0, int(round(sel_note_r.pkSmpSec * rr.rms_srate - contextSecs*rr.rms_srate)))
ei = min(rr.tdRmsDbV.shape[0],int(round(sel_note_r.pkSmpSec * rr.rms_srate + contextSecs*rr.rms_srate)))
rr.tdRmsDbV = rr.tdRmsDbV[bi:ei]
rr.rmsDbV = rr.rmsDbV[bi:ei]
offsSec = bi / rr.rms_srate
sL = []
for r in statsL:
begSmpIdx = int(round(r.begSmpSec * rr.rms_srate))
endSmpIdx = int(round(r.endSmpSec * rr.rms_srate))
if begSmpIdx > bi and endSmpIdx < ei:
r0 = r
r0.begSmpSec = r.begSmpSec - offsSec
r0.endSmpSec = r.endSmpSec - offsSec
r0.pkSmpSec = r.pkSmpSec - offsSec
if r.begSmpSec == sel_note_r.begSmpSec:
setattr(r0,"MIN",True)
sL.append(r0)
return rr,sL
def plot_note_analysis( inDir ):
rmsWndMs=300
rmsHopMs=30
dbRefWndMs=500
harmCandN=5
harmN=3
durDecayPct = 50
path = os.path.normpath(inDir)
pathL = inDir.split(os.sep)
take_id = int(pathL[-1])
midi_pitch = int(pathL[-2])
r = rms_analysis_main( inDir, midi_pitch, rmsWndMs=rmsWndMs, rmsHopMs=rmsHopMs, dbRefWndMs=dbRefWndMs, harmCandN=harmCandN, harmN=harmN, durDecayPct=durDecayPct )
r,statsL = select_min_note(r,r.statsL)
do_plot(r,statsL)
def plot_note_analysis_dir( inDir, dirL ):
for folder in dirL:
path = os.path.join(inDir,str(folder),"0")
plot_note_analysis( path )
def get_all_note_durations( inDir, cacheFn ):
folderL = os.listdir( inDir )
yL = []
for folder in folderL:
takeId = 0
rmsWndMs=300
rmsHopMs=30
dbRefWndMs=500
harmCandN=5
harmN=3
durDecayPct = 40
path = os.path.normpath(inDir)
midi_pitch = int( folder )
takePath = os.path.join(inDir,folder,str(takeId))
if os.path.isfile(os.path.join(takePath,'seq.json')):
print(midi_pitch)
r = rms_analysis_main( takePath, midi_pitch, rmsWndMs=rmsWndMs, rmsHopMs=rmsHopMs, dbRefWndMs=dbRefWndMs, harmCandN=harmCandN, harmN=harmN, durDecayPct=durDecayPct )
xL = []
for i,sr in enumerate(r.statsL):
xL.append((r.pkUsL[i],sr.durMs,sr.pkDb,sr.quality))
yL.append((midi_pitch,xL))
with open(cacheFn,"wb") as f:
pickle.dump(yL,f)
def plot_all_note_durations( cacheFn, pitchL=None, axN=12, yamlCfgFn=None, minDurMs=800, maxPulseUs=None ):
keyMapD = None
if yamlCfgFn is not None:
keyMapD = key_info_dictionary( keyMapL=None, yamlCfgFn=yamlCfgFn)
fig,axL = plt.subplots(axN,1)
fig.set_size_inches(18.5, 10.5*axN)
#cL = list(mcd.CSS4_COLORS.values())
cL = ['black','brown','orangered','saddlebrown','peru','olivedrab','lightgreen','springgreen','cadetblue','slategray','royalblue','navy','darkviolet','deeppink','crimson']
yD=[]
with open(cacheFn,"rb") as f:
yD = dict(pickle.load(f))
cn = 3 #min(1,len(cL)//len(yD))
ci = 0
i = 0
for midi_pitch in pitchL:
if (pitchL is not None and midi_pitch not in pitchL) or midi_pitch not in yD:
continue
xL = yD[midi_pitch]
pkUsL,durMsL,pkDbL,qualityL = tuple(zip(*xL))
if maxPulseUs is not None:
pkUsL = np.array(pkUsL)
pkUsL = pkUsL[ pkUsL < maxPulseUs ]
durMsL = durMsL[0:len(pkUsL)]
pkDbL = pkDbL[0:len(pkUsL)]
qualityL = qualityL[0:len(pkUsL)]
axi = i//(len(pitchL)//axN)
if keyMapD is None:
legendLabel = str(midi_pitch)
else:
legendLabel = getattr(keyMapD[midi_pitch],'type') + " " + getattr(keyMapD[midi_pitch],'class') + str(midi_pitch)
axL[axi].plot(pkUsL,durMsL,color=cL[ci],label=legendLabel)
# plot the quietest stable note
if minDurMs is not None:
sni = select_first_stable_note_by_dur( durMsL, minDurMs )
if sni is not None:
axL[axi].plot(pkUsL[sni],durMsL[sni],marker=".",color='red')
axL[axi].text(pkUsL[sni],durMsL[sni] + 50,"%4.1f" % pkDbL[sni])
sni = select_first_stable_note_by_delta_db( pkDbL, pkUsL )
if sni is not None:
axL[axi].plot(pkUsL[sni],durMsL[sni],marker=".",color='blue')
axL[axi].text(pkUsL[sni],durMsL[sni] + 50,"%4.1f" % pkDbL[sni])
ci += cn
if ci >= len(cL):
ci = ci - len(cL)
axL[axi].legend()
i+=1
plt.show()
def plot_quiet_note_db( cacheFn, yamlCfgFn, minDurMs=700 ):
keyMapD = key_info_dictionary( keyMapL=None, yamlCfgFn=yamlCfgFn)
yD=[]
with open(cacheFn,"rb") as f:
yD = dict(pickle.load(f))
dbL = []
for midi_pitch in range(24,108):
pk0Db = 0
pk1Db = 0
minDb = 0
if midi_pitch in yD:
xL = yD[midi_pitch]
pkUsL,durMsL,pkDbL,qualityL = tuple(zip(*xL))
# plot the quietest stable note
sni = select_first_stable_note_by_dur( durMsL, minDurMs )
if sni is not None:
pk0Db = pkDbL[sni]
sni = select_first_stable_note_by_delta_db( pkDbL, pkUsL )
if sni is not None:
pk1Db = pkDbL[sni]
minDb = min(pk0Db,pk1Db)
dbL.append( (midi_pitch, minDb, pk0Db,pk1Db) )
fig,ax = plt.subplots()
pitchL,minDbL,pk0DbL,pk1DbL = tuple(zip(*dbL))
ax.plot( pitchL, pk0DbL, label="dur" )
ax.plot( pitchL, pk1DbL, label="delta" )
#ax.plot( pitchL, minDbL, label="min" )
for i,pitch in enumerate(pitchL):
ax.text( pitch, pk0DbL[i]+1, "%i %s" % (pitch,getattr(keyMapD[pitch],'type')))
ax.axhline( np.mean(minDbL), label="mean", color="blue" )
ax.axhline( np.median(minDbL), label="median", color="green" )
ax.legend()
plt.show()
if __name__ == "__main__":
#inDir = sys.argv[1]
#plot_note_analysis( inDir )
pitchL = [ 30,31,32,33,34,35 ]
pitchL = [ 70,71,72,73,74,75 ]
#plot_note_analysis_dir( "/home/kevin/temp/p_ac_3c",pitchL)
durFn = "/home/kevin/temp/cache_note_dur.pickle"
#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")

View File

@ -3,6 +3,8 @@ import matplotlib.pyplot as plt
import numpy as np import numpy as np
from common import parse_yaml_cfg from common import parse_yaml_cfg
from rms_analysis import rms_analysis_main from rms_analysis import rms_analysis_main
from rms_analysis import select_first_stable_note_by_delta_db
from rms_analysis import select_first_stable_note_by_dur
def is_nanV( xV ): def is_nanV( xV ):
@ -29,29 +31,6 @@ def form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None ):
# append the midi pitch to the input directory # append the midi pitch to the input directory
#inDir = os.path.join( inDir, "%i" % (midi_pitch)) #inDir = os.path.join( inDir, "%i" % (midi_pitch))
if False:
# determine the take id if none was given
if take_id is None:
take_id = _find_max_take_id( inDir )
inDir = os.path.join(inDir,"%i" % (take_id))
assert( os.path.isdir(inDir))
# analyze the requested take audio
r = rms_analysis_main( inDir, midi_pitch, **analysisArgsD['rmsAnalysisArgs'] )
pkL = []
# store the peaks in pkL[ (db,us) ]
for db,us in zip(r.pkDbL,r.pkUsL):
pkL.append( (db,us) )
# sort the peaks on increasing attack pulse microseconds
pkL = sorted( pkL, key= lambda x: x[1] )
# split pkL
pkDbL,pkUsL = tuple(zip(*pkL))
dirL = os.listdir(inDir) dirL = os.listdir(inDir)
pkL = [] pkL = []
@ -123,7 +102,7 @@ def form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None ):
# print("Multi-value pulse locations were found during velocity table formation: ",multValL) # print("Multi-value pulse locations were found during velocity table formation: ",multValL)
pass pass
return pulseUsL,pulseDbL return pulseUsL,pulseDbL,r.holdDutyPctL
@ -132,7 +111,7 @@ def merge_close_sample_points( pkDbUsL, minSampleDistanceUs ):
avg0Us = np.mean(np.diff([ x[1] for x in pkDbUsL ])) avg0Us = np.mean(np.diff([ x[1] for x in pkDbUsL ]))
n0 = len(pkDbUsL) n0 = len(pkDbUsL)
while True: while True and n0>0:
us0 = None us0 = None
db0 = None db0 = None
@ -262,7 +241,7 @@ def plot_resample_pulse_times( inDir, analysisArgsD ):
newPulseUsL, rmsDbV, pulseUsL = form_resample_pulse_time_list( inDir, analysisArgsD ) newPulseUsL, rmsDbV, pulseUsL = form_resample_pulse_time_list( inDir, analysisArgsD )
midi_pitch = int( inDir.split("/")[-1] ) midi_pitch = int( inDir.split("/")[-1] )
velTblUsL,velTblDbL = form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None ) velTblUsL,velTblDbL,_ = form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None )
fig,ax = plt.subplots() fig,ax = plt.subplots()
@ -432,8 +411,14 @@ def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ):
# print beg/end boundaries # print beg/end boundaries
for i,(begMs, endMs) in enumerate(r.eventTimeL): for i,(begMs, endMs) in enumerate(r.eventTimeL):
pkSec = r.pkIdxL[i] / r.rms_srate
endSec = pkSec + r.statsL[i].durMs / 1000.0
ax.axvline( x=begMs/1000.0, color="green") ax.axvline( x=begMs/1000.0, color="green")
ax.axvline( x=endMs/1000.0, color="red") ax.axvline( x=endMs/1000.0, color="red")
ax.axvline( x=pkSec, color="black")
ax.axvline( x=endSec, color="black")
ax.text(begMs/1000.0, 20.0, str(i) ) ax.text(begMs/1000.0, 20.0, str(i) )
@ -448,10 +433,12 @@ def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ):
return r return r
def do_td_plot( inDir, analysisArgs ): def do_td_plot( inDir, analysisArgs ):
fig,axL = plt.subplots(2,1) fig,axL = plt.subplots(3,1)
fig.set_size_inches(18.5, 10.5, forward=True) fig.set_size_inches(18.5, 10.5, forward=True)
id = int(inDir.split("/")[-1]) id = int(inDir.split("/")[-1])
@ -459,8 +446,41 @@ def do_td_plot( inDir, analysisArgs ):
r = td_plot(axL[0],inDir,midi_pitch,id,analysisArgs) r = td_plot(axL[0],inDir,midi_pitch,id,analysisArgs)
axL[1].plot( r.pkUsL, r.pkDbL, marker='.' ) qualityV = np.array([ x.quality for x in r.statsL ]) * np.max(r.pkDbL)
durMsV = np.array([ x.durMs for x in r.statsL ])
avgV = np.array([ x.durAvgDb for x in r.statsL ])
#durMsV[ durMsV < 400 ] = 0
#durMsV = durMsV * np.max(r.pkDbL)/np.max(durMsV)
#durMsV = durMsV / 100.0
dV = np.diff(r.pkDbL) / r.pkDbL[1:]
axL[1].plot( r.pkUsL, r.pkDbL, marker='.',label="pkDb" )
axL[1].plot( r.pkUsL, qualityV, marker='.',label="quality" )
axL[1].plot( r.pkUsL, avgV, marker='.',label="avgDb" )
#axL[2].plot( r.pkUsL, durMsV, marker='.' )
axL[2].plot( r.pkUsL[1:], dV, marker='.',label='d')
axL[2].set_ylim([-1,1])
axL[1].legend()
sni = select_first_stable_note_by_dur( durMsV )
if sni is not None:
axL[1].plot( r.pkUsL[sni], r.pkDbL[sni], marker='*', color='red')
sni = select_first_stable_note_by_delta_db( r.pkDbL )
if sni is not None:
axL[2].plot( r.pkUsL[sni], dV[sni-1], marker='*', color='red')
for i,s in enumerate(r.statsL):
axL[1].text( r.pkUsL[i], r.pkDbL[i] + 1, "%i" % (i))
for i in range(1,len(r.pkUsL)):
axL[2].text( r.pkUsL[i], dV[i-1], "%i" % (i))
plt.show() plt.show()
def do_td_multi_plot( inDir, analysisArgs ): def do_td_multi_plot( inDir, analysisArgs ):

View File

@ -1,7 +1,8 @@
import os,types,json import os,types,json,pickle
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
from common import parse_yaml_cfg
def calc_harm_bins( srate, binHz, midiPitch, harmN ): def calc_harm_bins( srate, binHz, midiPitch, harmN ):
@ -91,6 +92,8 @@ def audio_harm_rms( srate, xV, rmsWndMs, hopMs, dbRefWndMs, midiPitch, harmCandN
hopSmpN = int(round( hopMs * srate / 1000.0)) hopSmpN = int(round( hopMs * srate / 1000.0))
binHz = srate / wndSmpN binHz = srate / wndSmpN
#print( "STFT:", rmsWndMs, hopMs, wndSmpN, hopSmpN, wndSmpN-hopSmpN )
f,t,xM = stft( xV, fs=srate, window="hann", nperseg=wndSmpN, noverlap=wndSmpN-hopSmpN, return_onesided=True ) f,t,xM = stft( xV, fs=srate, window="hann", nperseg=wndSmpN, noverlap=wndSmpN-hopSmpN, return_onesided=True )
@ -115,7 +118,120 @@ def audio_harm_rms( srate, xV, rmsWndMs, hopMs, dbRefWndMs, midiPitch, harmCandN
rmsV = rms_to_db( rmsV, rms_srate, dbRefWndMs ) rmsV = rms_to_db( rmsV, rms_srate, dbRefWndMs )
return rmsV, rms_srate, binHz return rmsV, rms_srate, binHz
def measure_duration_ms( rmsV, rms_srate, peak_idx, end_idx, decay_pct ):
"""
Calcuate the time it takes for a note to decay from the peak at
rmsV[peak_idx] dB to 'decay_pct' percent of the peak value.
"""
pkRmsDb = rmsV[ peak_idx ]
# calc the note turn-off (offset) db as a percentage of the peak amplitude
offsetRmsDb = pkRmsDb * decay_pct / 100.0
# calc the sample index where the note is off
offset_idx = peak_idx + np.argmin( np.abs(rmsV[peak_idx:end_idx] - offsetRmsDb) )
# calc the duration of the note
dur_ms = int(round((offset_idx - peak_idx) * 1000.0 / rms_srate))
#print(pkRmsDb, offsetRmsDb, peak_idx, offset_idx, end_idx, dur_ms, rms_srate)
return dur_ms
def select_first_stable_note_by_dur( durMsL, minDurMs=800 ):
first_stable_idx = None
for i,durMs in enumerate(durMsL):
if durMs > minDurMs and first_stable_idx is None:
first_stable_idx = i
else:
if durMs < minDurMs:
first_stable_idx = None
return first_stable_idx
def select_first_stable_note_by_delta_db_1( pkDbL, pkUsL, maxPulseUs=0.1 ):
wndN = 5
aL = []
dV = np.diff(pkDbL) / pkDbL[1:]
for ei in range(wndN,len(pkDbL)):
xV = dV[ei-wndN:ei]
avg = np.mean(np.abs(xV))
aL.append(avg)
k = np.argmin(np.abs(np.array(pkUsL) - maxPulseUs))
print(aL)
print(k)
for i in range(k,0,-1):
if aL[i] > maxDeltaDb:
return i + 1
return None
def select_first_stable_note_by_delta_db( pkDbL, pkUsL=None, maxPulseUs=0.1 ):
wndN = 5
dV = np.diff(pkDbL) / pkDbL[1:]
for ei in range(wndN,len(pkDbL)):
xV = dV[ei-wndN:ei]
avg = np.mean(np.abs(xV))
if avg < .1:
return (ei-wndN)+1
return None
def note_stats( r, decay_pct=50.0, extraDurSearchMs=500 ):
statsL = []
srate = r.rms_srate
qmax = 0
for i,(begSmpMs, endSmpMs) in enumerate(r.eventTimeL):
begSmpIdx = int(round(srate * begSmpMs / 1000.0))
endSmpIdx = int(round(srate * (endSmpMs + extraDurSearchMs) / 1000.0))
pkSmpIdx = r.pkIdxL[i]
durMs = measure_duration_ms( r.rmsDbV, srate, pkSmpIdx, endSmpIdx, decay_pct )
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
durAvgDb = (np.mean(r.rmsDbV[bi:ei]) + np.mean(r.tdRmsDbV[bi:ei]))/2.0
statsL.append( types.SimpleNamespace(**{'begSmpSec':begSmpIdx/srate,'endSmpSec':endSmpIdx/srate,'pkSmpSec':pkSmpIdx/srate,'durMs':durMs, 'pkDb':r.pkDbL[i], 'pulse_us':r.pkUsL[i], 'quality':qualityCoeff, 'durAvgDb':durAvgDb }))
for i,r in enumerate(statsL):
statsL[i].quality /= qmax
return statsL
def locate_peak_indexes( xV, xV_srate, eventMsL ): def locate_peak_indexes( xV, xV_srate, eventMsL ):
@ -130,10 +246,21 @@ def locate_peak_indexes( xV, xV_srate, eventMsL ):
return pkIdxL return pkIdxL
def key_info_dictionary( keyMapL=None, yamlCfgFn=None):
if yamlCfgFn is not None:
cfg = parse_yaml_cfg(yamlCfgFn)
keyMapL = cfg.key_mapL
kmD = {}
for d in keyMapL:
kmD[ d['midi'] ] = types.SimpleNamespace(**d)
return kmD
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 ):
seqFn = os.path.join( inDir, "seq.json") seqFn = os.path.join( inDir, "seq.json")
audioFn = os.path.join( inDir, "audio.wav") audioFn = os.path.join( inDir, "audio.wav")
@ -154,6 +281,15 @@ def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=
pkIdxL = locate_peak_indexes( rmsDbV, rms_srate, r['eventTimeL'] ) pkIdxL = locate_peak_indexes( rmsDbV, rms_srate, r['eventTimeL'] )
holdDutyPctL = None
if 'holdDutyPct' in r:
holdDutyPctL = [ (0, r['holdDutyPct']) ]
else:
holdDutyPctL = r['holdDutyPctL']
r = types.SimpleNamespace(**{ r = types.SimpleNamespace(**{
"audio_srate":srate, "audio_srate":srate,
"tdRmsDbV": tdRmsDbV, "tdRmsDbV": tdRmsDbV,
@ -166,7 +302,48 @@ def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=
#"min_pk_idx":min_pk_idx, #"min_pk_idx":min_pk_idx,
#"max_pk_idx":max_pk_idx, #"max_pk_idx":max_pk_idx,
"eventTimeL":r['eventTimeL'], "eventTimeL":r['eventTimeL'],
"holdDutyPctL":holdDutyPctL,
'pkDbL': [ rmsDbV[ i ] for i in pkIdxL ], 'pkDbL': [ rmsDbV[ i ] for i in pkIdxL ],
'pkUsL':r['pulseUsL'] }) 'pkUsL':r['pulseUsL'] })
statsL = note_stats(r,durDecayPct)
setattr(r,"statsL", statsL )
return r return r
def rms_analysis_main_all( inDir, cacheFn, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=500, harmCandN=5, harmN=3, durDecayPct=40 ):
if os.path.isfile(cacheFn):
print("READING analysis cache file: %s" % (cacheFn))
with open(cacheFn,"rb") as f:
rD = pickle.load(f)
return rD
folderL = os.listdir(inDir)
rD = {}
for folder in folderL:
pathL = folder.split(os.sep)
midi_pitch = int(pathL[-1])
print(midi_pitch)
path = os.path.join(inDir,folder,'0')
if os.path.isdir(path) and os.path.isfile(os.path.join(os.path.join(path,"seq.json"))):
r = rms_analysis_main( path, midi_pitch, rmsWndMs=rmsWndMs, rmsHopMs=rmsHopMs, dbRefWndMs=dbRefWndMs, harmCandN=harmCandN, harmN=harmN, durDecayPct=durDecayPct )
rD[ midi_pitch ] = r
with open(cacheFn,"wb") as f:
pickle.dump(rD,f)
return rD

76
rt_note_analysis.py Normal file
View File

@ -0,0 +1,76 @@
import types
import numpy as np
from rms_analysis import audio_harm_rms
from rms_analysis import audio_rms
from rms_analysis import locate_peak_indexes
from rms_analysis import measure_duration_ms
class RT_Analyzer:
def __init__(self):
self.td_dur_ms = 0
self.td_db = 0
self.hm_dur_ms = 0
self.hm_db = 0
def analyze_note( self, audioDev, midi_pitch, begTimeMs, endTimeMs, anlArgD ):
td_dur_ms = 0
td_db = 0
hm_dur_ms = 0
hm_db = 0
decay_pct = 50.0
result = audioDev.linear_buffer()
if result:
sigV = result.value
anlArgs = types.SimpleNamespace(**anlArgD)
rmsDbV, rms_srate, binHz = audio_harm_rms( audioDev.srate, np.squeeze(sigV), anlArgs.rmsWndMs, anlArgs.rmsHopMs, anlArgs.dbRefWndMs, midi_pitch, anlArgs.harmCandN, anlArgs.harmN )
pkIdxL = locate_peak_indexes( rmsDbV, rms_srate, [( begTimeMs, endTimeMs)] )
if len(pkIdxL) > 0:
end_idx = int(round(endTimeMs * rms_srate / 1000.0))
if end_idx > pkIdxL[0]:
hm_dur_ms = measure_duration_ms( rmsDbV, rms_srate, pkIdxL[0], end_idx, decay_pct )
hm_db = rmsDbV[ pkIdxL[0] ]
tdRmsDbV, rms0_srate = audio_rms( audioDev.srate, np.squeeze(sigV), anlArgs.rmsWndMs, anlArgs.rmsHopMs, anlArgs.dbRefWndMs )
tdPkIdxL = locate_peak_indexes( tdRmsDbV, rms0_srate, [( begTimeMs, endTimeMs)] )
if len(tdPkIdxL):
end_idx = int(round(endTimeMs * rms0_srate / 1000.0))
if end_idx > tdPkIdxL[0]:
td_dur_ms = measure_duration_ms( tdRmsDbV, rms0_srate, tdPkIdxL[0], end_idx, decay_pct )
td_db = tdRmsDbV[ tdPkIdxL[0] ]
td_d_ms = td_dur_ms - self.td_dur_ms
td_d_db = td_db - self.td_db
hm_d_ms = hm_dur_ms - self.hm_dur_ms
hm_d_db = hm_db - self.hm_db
print("DUR: %5.2f %5.2f d:%5.2f %5.2f dB | %i %i d:%i %i ms" % (hm_db, td_db, hm_d_db, td_d_db, hm_dur_ms, td_dur_ms, hm_d_ms, td_d_ms) )
self.td_dur_ms = td_dur_ms
self.td_db = td_db
self.hm_dur_ms = hm_dur_ms
self.hm_db = hm_db