123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559 |
- ##| Copyright: (C) 2019-2020 Kevin Larke <contact AT larke DOT org>
- ##| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file.
- import os, sys
- 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
- from rms_analysis import samples_to_linear_residual
-
- def is_nanV( xV ):
-
- for i in range(xV.shape[0]):
- if np.isnan( xV[i] ):
- return True
-
- return False
-
- def _find_max_take_id( inDir ):
-
- id = 0
- while os.path.isdir( os.path.join(inDir, "%i" % id) ):
- id += 1
-
- if id > 0:
- id -= 1
-
- return id
-
-
- 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))
-
- dirL = os.listdir(inDir)
-
- pkL = []
-
- # for each take in this directory
- for idir in dirL:
-
- take_number = int(idir)
-
-
- if not os.path.isfile(os.path.join( inDir,idir, "seq.json")):
- continue
-
- # analyze this takes audio and locate the note peaks
- r = rms_analysis_main( os.path.join(inDir,idir), midi_pitch, **analysisArgsD['rmsAnalysisArgs'] )
-
- # 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] )
-
- # merge sample points that separated by less than 'minSampleDistUs' milliseconds
- pkL = merge_close_sample_points( pkL, analysisArgsD['minSampleDistUs'] )
-
- # split pkL
- pkDbL,pkUsL = tuple(zip(*pkL))
-
- #-------------------------------------------
-
- # locate the first and last note
- min_pk_idx, max_pk_idx = find_min_max_peak_index( pkDbL, analysisArgsD['minAttkDb'], analysisArgsD['maxDbOffset'] )
-
- db1 = pkDbL[ max_pk_idx ]
- db0 = pkDbL[ min_pk_idx ]
-
- pulseUsL = []
- pulseDbL = []
- multValL = []
- for out_idx in range(128):
-
- # calc the target volume
- db = db0 + (out_idx * (db1-db0)/127.0)
-
- multi_value_count = 0
-
- # look for the target between each of the sampled points
- for i in range(1,len(pkDbL)):
-
- # if the target volume is between these two sample points
- if pkDbL[i-1] <= db and db < pkDbL[i]:
-
- # if the target has not already been located
- if len(pulseUsL) == out_idx:
-
- # interpolate the pulse time from between the sampled points
- frac = (db - pkDbL[i-1]) / (pkDbL[i] - pkDbL[i-1])
- us = pkUsL[i-1] + frac * (pkUsL[i] - pkUsL[i-1])
- db = pkDbL[i-1] + frac * (pkDbL[i] - pkDbL[i-1])
- pulseUsL.append(us)
- pulseDbL.append(db)
-
- else:
- # this target db value was found between multiple sampled points
- # therefore the sampled volume function is not monotonic
- multi_value_count += 1
-
- if multi_value_count > 0:
- multValL.append((out_idx,multi_value_count))
-
- if len(multValL) > 0:
- # print("Multi-value pulse locations were found during velocity table formation: ",multValL)
- pass
-
- return pulseUsL,pulseDbL,r.holdDutyPctL
-
-
-
- def merge_close_sample_points( pkDbUsL, minSampleDistanceUs ):
-
- avg0Us = np.mean(np.diff([ x[1] for x in pkDbUsL ]))
- n0 = len(pkDbUsL)
-
- while True and n0>0:
- us0 = None
- db0 = None
-
- for i,(db,us) in enumerate(pkDbUsL):
- if i > 0 and us - us0 < minSampleDistanceUs:
- us1 = (us0 + us)/2
- db1 = (db0 + db)/2
- pkDbUsL[i-1] = (db1,us1)
- del pkDbUsL[i]
- break
- else:
- us0 = us
- db0 = db
-
- if i+1 == len(pkDbUsL):
- break
-
- avg1Us = np.mean(np.diff([ x[1] for x in pkDbUsL ]))
-
- print("%i sample points deleted by merging close points." % (n0 - len(pkDbUsL)))
- print("Mean time between samples - before:%f after:%f " % (avg0Us,avg1Us))
- print("Min time between samples: %i " % (np.min(np.diff([x[1] for x in pkDbUsL]))))
-
- return pkDbUsL
-
-
- def _calc_resample_points( dPkDb, pkUs0, pkUs1, samplePerDb, minSampleDistUs ):
-
- dPkUs = pkUs1 - pkUs0
- sampleCnt = max(int(round(abs(dPkDb) * samplePerDb)),samplePerDb)
- dUs = max(int(round(dPkUs/sampleCnt)),minSampleDistUs)
- sampleCnt = int(round(dPkUs/dUs))
- dUs = int(round(dPkUs/sampleCnt))
- usL = [ pkUs0 + dUs*j for j in range(sampleCnt+1)]
-
- return usL
-
- def calc_resample_ranges( pkDbL, pkUsL, min_pk_idx, max_pk_idx, maxDeltaDb, samplePerDb, minSampleDistUs ):
-
- if min_pk_idx == 0:
- print("No silent notes were generated. Decrease the minimum peak level or the hold voltage.")
- return None
-
- resampleUsSet = set()
- refPkDb = pkDbL[min_pk_idx]
-
- #pkDbL = pkDbL[ pkIdxL ]
-
- for i in range( min_pk_idx, max_pk_idx+1 ):
-
- d = pkDbL[i] - pkDbL[i-1]
-
- usL = []
-
- # if this peak is less than maxDeltaDb above the previous pk or
- # it is below the previous max peak
- if d > maxDeltaDb or d <= 0 or pkDbL[i] < refPkDb:
-
- usL = _calc_resample_points( d, pkUsL[i-1], pkUsL[i], samplePerDb, minSampleDistUs )
-
- if d <= 0 and i + 1 < len(pkDbL):
- d = pkDbL[i+1] - pkDbL[i]
-
- usL += _calc_resample_points( d, pkUsL[i-1], pkUsL[i], samplePerDb, minSampleDistUs )
-
-
- if pkDbL[i] > refPkDb:
- refPkDb = pkDbL[i]
-
- if usL:
- resampleUsSet = resampleUsSet.union( usL )
-
- return resampleUsSet
-
-
-
- def form_resample_pulse_time_list( inDir, analysisArgsD ):
- """" This function merges all available data from previous takes to form
- a new list of pulse times to sample.
- """
-
- # the last folder is always the midi pitch of the note under analysis
- midi_pitch = int( inDir.split("/")[-1] )
-
- dirL = os.listdir(inDir)
-
- pkL = []
-
- # for each take in this directory
- for idir in dirL:
-
- take_number = int(idir)
-
- # analyze this takes audio and locate the note peaks
- r = rms_analysis_main( os.path.join(inDir,idir), midi_pitch, **analysisArgsD['rmsAnalysisArgs'] )
-
- # 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] )
-
- # merge sample points that separated by less than 'minSampleDistUs' milliseconds
- pkL = merge_close_sample_points( pkL, analysisArgsD['minSampleDistUs'] )
-
- # split pkL
- pkDbL,pkUsL = tuple(zip(*pkL))
-
-
- # locate the first and last note
- min_pk_idx, max_pk_idx = find_min_max_peak_index( pkDbL, analysisArgsD['minAttkDb'], analysisArgsD['maxDbOffset'] )
-
- # estimate the microsecond locations to resample
- resampleUsSet = calc_resample_ranges( pkDbL, pkUsL, min_pk_idx, max_pk_idx, analysisArgsD['maxDeltaDb'], analysisArgsD['samplesPerDb'], analysisArgsD['minSampleDistUs'] )
-
- resampleUsL = sorted( list(resampleUsSet) )
-
- #print(resampleUsL)
-
- return resampleUsL, pkDbL, pkUsL
-
- def plot_curve( ax, pulseUsL, rmsDbV ):
-
- coeff = np.polyfit(pulseUsL,rmsDbV,5)
- func = np.poly1d(coeff)
-
- ax.plot( pulseUsL, func(pulseUsL), color='red')
-
- def plot_resample_pulse_times_0( 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 )
-
- fig,ax = plt.subplots()
-
- ax.plot(pulseUsL,rmsDbV,marker='.' )
-
- for us in newPulseUsL:
- ax.axvline( x = us )
-
- ax.plot(velTblUsL,velTblDbL,marker='.',linestyle='None')
-
-
- plt.show()
-
- 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 )
-
- fig,axL = plt.subplots(2,1,gridspec_kw={'height_ratios': [2, 1]})
-
- axL[0].plot(pulseUsL,rmsDbV,marker='.' )
-
- #plot_curve( ax, velTblUsL,velTblDbL)
-
- scoreV = samples_to_linear_residual( pulseUsL, rmsDbV)
-
- axL[0].plot(pulseUsL,rmsDbV + scoreV)
- axL[0].plot(pulseUsL,rmsDbV + np.power(scoreV,2.0))
- axL[0].plot(pulseUsL,rmsDbV - np.power(scoreV,2.0))
-
- axL[1].axhline(0.0,color='black')
- axL[1].axhline(1.0,color='black')
- axL[1].plot(pulseUsL,np.abs(scoreV * 100.0 / rmsDbV))
- axL[1].set_ylim((0.0,50))
- plt.show()
-
-
-
- def find_min_max_peak_index( pkDbL, minDb, maxDbOffs ):
- """
- Find the min db and max db peak.
- """
- # select only the peaks from rmsV[] to work with
- yV = pkDbL
-
- # get the max volume note
- max_i = np.argmax( yV )
- maxDb = yV[ max_i ]
-
- min_i = max_i
-
- # starting from the max volume peak go backwards
- for i in range( max_i, 0, -1 ):
-
- # if this peak is within maxDbOffs of the loudest then choose this one instead
- #if maxDb - yV[i] < maxDbOffs:
- # max_i = i
-
- # if this peak is less than minDb then the previous note is the min note
- if yV[i] < minDb:
- break
-
- min_i = i
-
- if min_i >= max_i:
- min_i = 0
- max_i = len(pkDbL)-1
-
-
- if min_i == 0:
- print("No silent notes were generated. Decrease the minimum peak level or the hold voltage.")
-
- return min_i, max_i
-
- def find_skip_peaks( rmsV, pkIdxL, min_pk_idx, max_pk_idx ):
- """ Fine peaks associated with longer attacks pulses that are lower than peaks with a shorter attack pulse.
- These peaks indicate degenerate portions of the pulse/db curve which must be skipped during velocity table formation
- """
- skipPkIdxL = []
- yV = rmsV[pkIdxL]
- refPkDb = yV[min_pk_idx]
-
- for i in range( min_pk_idx+1, max_pk_idx+1 ):
- if yV[i] > refPkDb:
- refPkDb = yV[i]
- else:
- skipPkIdxL.append(i)
-
-
- return skipPkIdxL
-
- def find_out_of_range_peaks( rmsV, pkIdxL, min_pk_idx, max_pk_idx, maxDeltaDb ):
- """ Locate peaks which are more than maxDeltaDb from the previous peak.
- If two peaks are separated by more than maxDeltaDb then the range must be resampled
- """
-
- oorPkIdxL = []
- yV = rmsV[pkIdxL]
-
- for i in range( min_pk_idx, max_pk_idx+1 ):
- if i > 0:
- d = yV[i] - yV[i-1]
- if d > maxDeltaDb or d < 0:
- oorPkIdxL.append(i)
-
- return oorPkIdxL
-
-
-
- def plot_spectrum( ax, srate, binHz, specV, midiPitch, harmN ):
- """ Plot a single spectrum, 'specV' and the harmonic peak location boundaries."""
-
- binN = specV.shape[0]
- harmLBinL,harmMBinL,harmUBinL = calc_harm_bins( srate, binHz, midiPitch, harmN )
-
- fundHz = harmMBinL[0] * binHz
- maxPlotHz = fundHz * (harmN+1)
- maxPlotBinN = int(round(maxPlotHz/binHz))
-
- hzV = np.arange(binN) * (srate/(binN*2))
-
- specV = 20.0 * np.log10(specV)
-
- ax.plot(hzV[0:maxPlotBinN], specV[0:maxPlotBinN] )
-
- for h0,h1,h2 in zip(harmLBinL,harmMBinL,harmUBinL):
- ax.axvline( x=h0 * binHz, color="blue")
- ax.axvline( x=h1 * binHz, color="black")
- ax.axvline( x=h2 * binHz, color="blue")
-
- ax.set_ylabel(str(midiPitch))
-
- def plot_spectral_ranges( inDir, pitchL, rmsWndMs=300, rmsHopMs=30, harmN=5, dbLinRef=0.001 ):
- """ Plot the spectrum from one note (7th from last) in each attack pulse length sequence referred to by pitchL."""
-
- plotN = len(pitchL)
- fig,axL = plt.subplots(plotN,1)
-
- for plot_idx,midiPitch in enumerate(pitchL):
-
- # get the audio and meta-data file names
- seqFn = os.path.join( inDir, str(midiPitch), "seq.json")
- audioFn = os.path.join( inDir, str(midiPitch), "audio.wav")
-
- # read the meta data object
- with open( seqFn, "rb") as f:
- r = json.load(f)
-
- # read the audio file
- srate, signalM = wavfile.read(audioFn)
- sigV = signalM / float(0x7fff)
-
- # calc. the RMS envelope in the time domain
- rms0DbV, rms0_srate = audio_rms( srate, sigV, rmsWndMs, rmsHopMs, dbLinRef )
-
- # locate the sample index of the peak of each note attack
- pkIdx0L = locate_peak_indexes( rms0DbV, rms0_srate, r['eventTimeL'] )
-
- # select the 7th to last note for spectrum measurement
-
- #
- # TODO: come up with a better way to select the note to measure
- #
- spectrumSmpIdx = pkIdx0L[ len(pkIdx0L) - 7 ]
-
-
- # calc. the RMS envelope by taking the max spectral peak in each STFT window
- rmsDbV, rms_srate, specV, specHopIdx, binHz = audio_stft_rms( srate, sigV, rmsWndMs, rmsHopMs, dbLinRef, spectrumSmpIdx)
-
- # specV[] is the spectrum of the note at spectrumSmpIdx
-
- # plot the spectrum and the harmonic selection ranges
- plot_spectrum( axL[plot_idx], srate, binHz, specV, midiPitch, harmN )
- plt.show()
-
-
-
- def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ):
-
- r = rms_analysis_main( inDir, midi_pitch, **analysisArgsD['rmsAnalysisArgs'] )
-
- min_pk_idx, max_pk_idx = find_min_max_peak_index( r.pkDbL, analysisArgsD['minAttkDb'], analysisArgsD['maxDbOffset'] )
-
- skipPkIdxL = find_skip_peaks( r.rmsDbV, r.pkIdxL, min_pk_idx, max_pk_idx )
-
- jmpPkIdxL = find_out_of_range_peaks( r.rmsDbV, r.pkIdxL, min_pk_idx, max_pk_idx, analysisArgsD['maxDeltaDb'] )
-
- secV = np.arange(0,len(r.rmsDbV)) / r.rms_srate
-
- ax.plot( secV, r.rmsDbV )
- ax.plot( np.arange(0,len(r.tdRmsDbV)) / r.rms_srate, r.tdRmsDbV, color="black" )
-
-
- # 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) )
-
-
- # plot peak markers
- for i,pki in enumerate(r.pkIdxL):
- marker = 4 if i==min_pk_idx or i==max_pk_idx else 5
- color = "red" if i in skipPkIdxL else "black"
- ax.plot( [pki / r.rms_srate], [ r.rmsDbV[pki] ], marker=marker, color=color)
-
- if i in jmpPkIdxL:
- ax.plot( [pki / r.rms_srate], [ r.rmsDbV[pki] ], marker=6, color="blue")
-
-
- return r
-
-
-
- def do_td_plot( inDir, analysisArgs ):
-
- fig,axL = plt.subplots(3,1)
- fig.set_size_inches(18.5, 10.5, forward=True)
-
- id = int(inDir.split("/")[-1])
- midi_pitch = int(inDir.split("/")[-2])
-
- r = td_plot(axL[0],inDir,midi_pitch,id,analysisArgs)
-
- 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()
-
- def do_td_multi_plot( inDir, analysisArgs ):
-
- midi_pitch = int(inDir.split("/")[-1])
-
- dirL = os.listdir(inDir)
-
- fig,axL = plt.subplots(len(dirL),1)
-
-
- for id,(idir,ax) in enumerate(zip(dirL,axL)):
-
- td_plot(ax, os.path.join(inDir,str(id)), midi_pitch, id, analysisArgs )
-
- plt.show()
-
-
- if __name__ == "__main__":
-
- inDir = sys.argv[1]
- cfgFn = sys.argv[2]
- take_id = None if len(sys.argv)<4 else sys.argv[3]
-
- cfg = parse_yaml_cfg( cfgFn )
-
- if take_id is not None:
- inDir = os.path.join(inDir,take_id)
- do_td_plot(inDir,cfg.analysisArgs)
- else:
- #do_td_multi_plot(inDir,cfg.analysisArgs)
-
- #plot_spectral_ranges( inDir, [ 24, 36, 48, 60, 72, 84, 96, 104] )
-
- plot_resample_pulse_times( inDir, cfg.analysisArgs )
|