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:
parent
919ecadf8d
commit
26b997811a
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*.pyc
|
||||
.ipynb_checkpoints
|
@ -191,7 +191,9 @@ class AudioDevice(object):
|
||||
|
||||
def linear_buffer( self ):
|
||||
|
||||
smpN = self.buffer_sample_count()
|
||||
r = self.buffer_sample_count()
|
||||
if r:
|
||||
smpN = r.value
|
||||
bV = np.zeros( (smpN,self.ch_cnt) )
|
||||
|
||||
bi = 0
|
||||
|
209
keyboard.py
Normal file
209
keyboard.py
Normal 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)
|
||||
|
66
p_ac.py
66
p_ac.py
@ -10,11 +10,14 @@ 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
|
||||
|
||||
class AttackPulseSeq:
|
||||
""" 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.api = api
|
||||
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.noteDurMs = noteDurMs # duration of each chord in milliseconds
|
||||
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.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.beginMs = 0
|
||||
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.pitchL = pitchL # chord to play
|
||||
self.pulseUsL = pulseUsL # one onset pulse length in microseconds per sequence element
|
||||
|
||||
self.holdDutyPctL = holdDutyPctL
|
||||
self.pulse_idx = 0
|
||||
self.state = 'note_on'
|
||||
self.prevHoldDutyPct = None
|
||||
@ -49,14 +53,15 @@ class AttackPulseSeq:
|
||||
# self.api.set_pwm_duty( pitch, self.holdDutyPct )
|
||||
# print("set PWM:%i"%(self.holdDutyPct))
|
||||
|
||||
if not playOnlyFl:
|
||||
# kpl if not playOnlyFl:
|
||||
self.audio.record_enable(True) # start recording audio
|
||||
|
||||
self.tick(ms) # play the first note
|
||||
|
||||
def stop(self, ms):
|
||||
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._disable() # disable this sequencer
|
||||
@ -69,8 +74,6 @@ class AttackPulseSeq:
|
||||
|
||||
def tick(self, ms):
|
||||
|
||||
self.audio.tick(ms)
|
||||
|
||||
# if next event time has arrived
|
||||
if self.is_enabled() and ms >= self.next_ms:
|
||||
|
||||
@ -120,20 +123,25 @@ class AttackPulseSeq:
|
||||
pulse_usec = int(self.pulseUsL[ self.pulse_idx ])
|
||||
self._set_duty_cycle( 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 ):
|
||||
self.eventTimeL[ self.pulse_idx ][1] = self.audio.buffer_sample_ms().value
|
||||
self.next_ms = ms + self.pauseDurMs
|
||||
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()
|
||||
|
||||
|
||||
def _send_note_off( self ):
|
||||
for pitch in self.pitchL:
|
||||
self.api.note_off( pitch )
|
||||
print("note-off:",pitch,self.pulse_idx)
|
||||
#print("note-off:",pitch,self.pulse_idx)
|
||||
|
||||
def _disable(self):
|
||||
self.state = None
|
||||
@ -167,7 +175,7 @@ class AttackPulseSeq:
|
||||
class CalibrateKeys:
|
||||
def __init__(self, cfg, audioDev, api):
|
||||
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.chordL = None
|
||||
@ -233,8 +241,10 @@ class CalibrateKeys:
|
||||
if outDir_id != 0:
|
||||
self.pulseUsL,_,_ = form_resample_pulse_time_list( outDir, self.cfg.analysisArgs )
|
||||
|
||||
holdDutyPctL = self.cfg.holdDutyPctL
|
||||
|
||||
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']
|
||||
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)
|
||||
|
||||
# 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 ):
|
||||
@ -265,6 +275,7 @@ class App:
|
||||
self.audioDev = None
|
||||
self.api = None
|
||||
self.calibrate = None
|
||||
self.keyboard = None
|
||||
|
||||
def setup( self, cfg ):
|
||||
self.cfg = cfg
|
||||
@ -294,12 +305,18 @@ class App:
|
||||
|
||||
self.calibrate = CalibrateKeys( cfg, self.audioDev, self.api )
|
||||
|
||||
self.keyboard = Keyboard( cfg, self.audioDev, self.api )
|
||||
|
||||
return res
|
||||
|
||||
def tick( self, ms ):
|
||||
|
||||
self.audioDev.tick(ms)
|
||||
|
||||
if self.calibrate:
|
||||
self.calibrate.tick(ms)
|
||||
if self.keyboard:
|
||||
self.keyboard.tick(ms)
|
||||
|
||||
def audio_dev_list( self, ms ):
|
||||
portL = self.audioDev.get_port_list( True )
|
||||
@ -315,8 +332,23 @@ class App:
|
||||
chordL = [ [pitch] for pitch in range(pitchRangeL[0], pitchRangeL[1]+1)]
|
||||
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 ):
|
||||
self.calibrate.stop(ms)
|
||||
self.keyboard.stop(ms)
|
||||
|
||||
def quit( self, ms ):
|
||||
if self.api:
|
||||
@ -440,7 +472,11 @@ class Shell:
|
||||
'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. "},
|
||||
'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 ):
|
||||
@ -515,11 +551,9 @@ class Shell:
|
||||
# tokenize the command
|
||||
tokL = s.split(' ')
|
||||
|
||||
|
||||
# execute the command
|
||||
result = self._exec_cmd( tokL )
|
||||
|
||||
|
||||
# if this is the 'quit' command
|
||||
if tokL[0] == 'q':
|
||||
break
|
||||
|
11
p_ac.yml
11
p_ac.yml
@ -21,7 +21,7 @@
|
||||
outDir: "~/temp/p_ac_3c",
|
||||
noteDurMs: 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_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_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 ],
|
||||
|
||||
@ -56,7 +56,10 @@
|
||||
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
|
||||
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: [
|
||||
|
79
plot_all_note_durations.ipynb
Normal file
79
plot_all_note_durations.ipynb
Normal file
File diff suppressed because one or more lines are too long
300
plot_note_analysis.py
Normal file
300
plot_note_analysis.py
Normal 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")
|
76
plot_seq.py
76
plot_seq.py
@ -3,6 +3,8 @@ import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from common import parse_yaml_cfg
|
||||
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 ):
|
||||
|
||||
@ -29,29 +31,6 @@ def form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None ):
|
||||
# append the midi pitch to the input directory
|
||||
#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)
|
||||
|
||||
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)
|
||||
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 ]))
|
||||
n0 = len(pkDbUsL)
|
||||
|
||||
while True:
|
||||
while True and n0>0:
|
||||
us0 = None
|
||||
db0 = None
|
||||
|
||||
@ -262,7 +241,7 @@ def plot_resample_pulse_times( inDir, analysisArgsD ):
|
||||
newPulseUsL, rmsDbV, pulseUsL = form_resample_pulse_time_list( inDir, analysisArgsD )
|
||||
|
||||
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()
|
||||
|
||||
@ -432,8 +411,14 @@ def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ):
|
||||
|
||||
# print beg/end boundaries
|
||||
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=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) )
|
||||
|
||||
|
||||
@ -449,9 +434,11 @@ def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ):
|
||||
|
||||
return r
|
||||
|
||||
|
||||
|
||||
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)
|
||||
|
||||
id = int(inDir.split("/")[-1])
|
||||
@ -459,7 +446,40 @@ def do_td_plot( inDir, 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()
|
||||
|
||||
|
183
rms_analysis.py
183
rms_analysis.py
@ -1,7 +1,8 @@
|
||||
import os,types,json
|
||||
import os,types,json,pickle
|
||||
from scipy.io import wavfile
|
||||
from scipy.signal import stft
|
||||
import numpy as np
|
||||
from common import parse_yaml_cfg
|
||||
|
||||
|
||||
def calc_harm_bins( srate, binHz, midiPitch, harmN ):
|
||||
@ -92,6 +93,8 @@ def audio_harm_rms( srate, xV, rmsWndMs, hopMs, dbRefWndMs, midiPitch, harmCandN
|
||||
|
||||
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 )
|
||||
|
||||
harmLBinL,harmMBinL,harmUBinL = calc_harm_bins( srate, binHz, midiPitch, harmCandN )
|
||||
@ -115,6 +118,119 @@ def audio_harm_rms( srate, xV, rmsWndMs, hopMs, dbRefWndMs, midiPitch, harmCandN
|
||||
rmsV = rms_to_db( rmsV, rms_srate, dbRefWndMs )
|
||||
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 ):
|
||||
@ -130,10 +246,21 @@ def locate_peak_indexes( xV, xV_srate, eventMsL ):
|
||||
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 ):
|
||||
def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=500, harmCandN=5, harmN=3, durDecayPct=40 ):
|
||||
|
||||
seqFn = os.path.join( inDir, "seq.json")
|
||||
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'] )
|
||||
|
||||
|
||||
|
||||
holdDutyPctL = None
|
||||
if 'holdDutyPct' in r:
|
||||
holdDutyPctL = [ (0, r['holdDutyPct']) ]
|
||||
else:
|
||||
holdDutyPctL = r['holdDutyPctL']
|
||||
|
||||
|
||||
r = types.SimpleNamespace(**{
|
||||
"audio_srate":srate,
|
||||
"tdRmsDbV": tdRmsDbV,
|
||||
@ -166,7 +302,48 @@ def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=
|
||||
#"min_pk_idx":min_pk_idx,
|
||||
#"max_pk_idx":max_pk_idx,
|
||||
"eventTimeL":r['eventTimeL'],
|
||||
"holdDutyPctL":holdDutyPctL,
|
||||
'pkDbL': [ rmsDbV[ i ] for i in pkIdxL ],
|
||||
'pkUsL':r['pulseUsL'] })
|
||||
|
||||
|
||||
statsL = note_stats(r,durDecayPct)
|
||||
|
||||
setattr(r,"statsL", statsL )
|
||||
|
||||
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
76
rt_note_analysis.py
Normal 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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user