Compare commits

...

9 Commits

13 changed files with 531 additions and 160 deletions

View File

@ -18,7 +18,7 @@ class AudioDevice(object):
self.bufL = []
self.bufIdx = -1
self.srate = 0
self.ch_cnt = 1
self.ch_cnt = 2
def setup( self, **kwargs ):

View File

@ -1,2 +1,84 @@
* Picadae Calibration Programs
sudo dnf install python3-devel jack-audio-connection-kit-devel
prerequisites:
pyyaml
sounddevice
soundfile
pthon-rtmidi
export PYTHONPATH=${HOME}/src/picadae/control/app
python p_ac.py -c p_ac.yml
c 60 60 # caputre note 60 using the full_pulseL[] from the p_ac.yml
# plot third take of note 60
python plot_seq.py ~/temp/p_ac_3_oa/60 p_ac.yml 3
# record a calibration take: CalibrateKeys.start
uses cfg.full_pulseL[] for the pulse sequence
use cfg.calibrateArgs.holdDutyPctD{} for hold PWM duty cycle
# obsolete plotting function in calibrate_plot.py
python calibrate_plot.py ~/temp/p_ac_3_oa/60/2 p_ac.yml 60
![Plot Seq 1](doc/do_td_plot.png)
`python plot_seq.py ~/temp/p_ac_3_od p_ac.yml 60 10`
`do_td_plot(inDir,cfg.analysisArgs, pitch, take_id )`
![Multi Plot 1](doc/multi_plot.png)
`python plot_seq.py p_ac.yml ~/temp/p_ac_3_od td_multi_plot 60 3 60 4 60 5`
plot_seq.py `do_td_multi_plot(inDir,cfg.analysisArgs,[(36,4), (48,2)] )`
![Spectral Ranges](doc/plot_spectral_ranges.png)
`python plot_seq.py p_ac.yml ~/temp/p_ac_3_od plot_spectral_ranges 60 3 60 4`
![Multi Usec dB](doc/us_db.png)
`python plot_seq_1.py p_ac.yml ~/temp/p_ac_3_od us_db 84`
![Usec dB Spread](doc/us_db_map.png)
`python plot_seq_1.py p_ac.yml ~/temp/p_ac_3_od us_db_map 84 72`
![Min Max](doc/min_max_db.png)
`python plot_seq_1.py p_ac.yml ~/temp/p_ac_3_od min_max 36 48 60 72 84`
![Min Max 2](doc/min_max_db_2.png)
The last number in the list is the 'takeId'
`python plot_seq_1.py p_ac.yml ~/temp/p_ac_3_od min_max_2 36 48 60 72 84 2`
`python plot_seq_1.py p_ac.yml ~/temp/p_ac_3_od manual_db`
Interpolate across the min and max db values to form the min/max curves for the complete
set of keys. The anchor points for the curves are taken from cfg record
manuMinD,manualAnchorPitchMinDbL, and manualMaxDbL.
````
# select the event (takeId, eventIdx) to use to represent the min value for each pitch
manualMinD: {
36: [2, 10],
48: [2, 10],
60: [2, 10],
72: [2, 10],
84: [2, 10]
},
# leave 60 out of the min anchor point list
manualAnchorPitchMinDbL: [ 36,48,72,84 ],
manualAnchorPitchMaxDbL: [ 36,48,60,72,84 ],
````

BIN
do_td_plot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
doc/do_td_plot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
doc/manual_db.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
doc/min_max_db_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
doc/multi_plot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
doc/us_db_map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

156
p_ac.yml
View File

@ -3,8 +3,8 @@
# Audio device setup
audio_off: {
inPortLabel: "5 USB Audio CODEC:", #"HDA Intel PCH: CS4208", # "5 USB Audio CODEC:", #"5 USB Sound Device",
audio: {
inPortLabel: "8 USB Audio CODEC:", #"HDA Intel PCH: CS4208", # "5 USB Audio CODEC:", #"5 USB Sound Device",
outPortLabel: ,
},
@ -12,10 +12,10 @@
inMonitorFl: False,
outMonitorFl: False,
throughFl: False,
#inPortLabel: "Fastlane:Fastlane MIDI A",
#outPortLabel: "Fastlane:Fastlane MIDI A"
inPortLabel: "picadae:picadae MIDI 1",
outPortLabel: "picadae:picadae MIDI 1"
inPortLabel: "Fastlane:Fastlane MIDI A",
outPortLabel: "Fastlane:Fastlane MIDI A"
#inPortLabel: "picadae:picadae MIDI 1",
#outPortLabel: "picadae:picadae MIDI 1"
},
# Picadae API args
@ -23,18 +23,19 @@
serial_baud: 38400,
i2c_base_addr: 21,
prescaler_usec: 16,
pwm_div: 5,
serial_sync_timeout_ms: 10000,
# MeasureSeq args
outDir: "~/temp/p_ac_3g",
outDir: "~/temp/p_ac_3_oe",
noteDurMs: 500,
pauseDurMs: 500,
reversePulseListFl: True,
useFullPulseListFl: True,
maxSilentNoteCount: 4,
silentNoteMaxPulseUs: 15000,
silentNoteMinDurMs: 250,
silentNoteMinDurMs: 180, #250,
# Midi file player
midiFileFn: "/home/kevin/media/audio/midi/txt/round4.txt",
@ -57,8 +58,8 @@
full_pulse8L: [ 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_pulseL: [11000, 11075, 11150, 11225, 11300, 11375, 11450, 11525, 11600,11675, 11750, 11825, 11900, 11975, 12050, 12125, 12200, 12275,12350, 12425, 12500, 12575, 12650, 12725, 12800, 12875, 12950, 13025, 13100, 13175, 13250, 13325, 13400, 13475, 13550, 13625, 13700, 13775, 13850, 13925, 14000, 14075, 14150, 14225, 14300, 14375, 14450, 14525, 14600, 14675, 14750, 14825, 14900, 14975],
# full_pulse9L was the last pulse list used on Matt's piano
full_pulse9L: [11000, 11075, 11150, 11225, 11300, 11375, 11450, 11525, 11600,11675, 11750, 11825, 11900, 11975, 12050, 12125, 12200, 12275,12350, 12425, 12500, 12575, 12650, 12725, 12800, 12875, 12950, 13025, 13100, 13175, 13250, 13325, 13400, 13475, 13550, 13625, 13700, 13775, 13850, 13925, 14000, 14075, 14150, 14225, 14300, 14375, 14450, 14525, 14600, 14675, 14750, 14825, 14900, 14975],
full_pulse10L: [ 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 ],
@ -66,6 +67,35 @@
full_pulse12L: [ 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 ],
# pulse lists below this line were developed on Steinway D (11/14/20)
# 60
full_pulse13L: [8000, 8250, 8500, 8750, 9000, 9250, 9500, 9750, 10000, 10250, 10500, 10750, 11000, 11250, 11500, 11750, 12000, 12250, 12500, 12750, 13000, 13500, 14000, 14500, 15000, 15500, 16000, 16500, 17000, 17500, 18000, 18500, 19000, 19500, 20000 ],
full_pulse14L: [ 9000, 9125, 9250, 9375, 9500, 9625, 9750, 9875, 10000, 10125, 10250, 10375, 10500, 10625, 10750, 10875, 11000, 11125, 11250, 11375, 11500, 11625, 11750, 11875, 12000, 12250, 12500, 12750, 13000, 13500, 14000, 14500, 15000, 15500, 16000, 16500, 17000, 17500, 18000, 18500, 19000, 19500, 20000, 20500, 21000, 21500, 22000 ],
full_pulse15L: [ 8000, 9000,10000, 11000, 12000, 13000, 14000, 15000, 16000, 17000, 18000, 19000, 20000 ],
full_pulse16L: [ 8000, 8125, 8250, 8375, 8500, 8625, 8750, 8875, 9000, 9125, 9250, 9375, 9500, 9625, 9750, 9875, 10000, 10125, 10250, 10375, 10500, 10625, 10750, 10875, 11000, 11125, 11250, 11375, 11500, 11625, 11750, 11875, 12000, 12250, 12500, 12750, 13000, 13500, 14000, 14500, 15000, 15500, 16000, 16500, 17000, 17500, 18000, 18500, 19000, 19500, 20000, 20500, 21000, 21500, 22000 ],
full_pulse17L: [ 8100, 8150, 8200, 8250, 8300, 8350, 8400, 8450, 8500, 8550, 8600, 8650, 8700, 8750, 8800, 8850, 8900, 8950, 9000, 9125, 9250, 9375, 9500, 9625, 9750, 9875, 10000, 10125, 10250, 10375, 10500, 10625, 10750, 10875, 11000, 11125, 11250, 11375, 11500, 11625, 11750, 11875, 12000, 12250, 12500, 12750, 13000, 13500, 14000, 14500, 15000, 15500, 16000, 16500, 17000, 17500, 18000, 18500, 19000, 19500, 20000, 20500, 21000, 21500, 22000 ],
full_pulse18L: [ 11500, 11625, 11750, 11875, 12000, 12250, 12500, 12750, 13000, 13500, 14000, 14500, 15000, 15500 ],
full_pulse19L: [ 11875, 12000, 12125, 12250, 12375, 12500, 12625, 12750, 12875, 13000, 13250, 13500, 13750, 14000, 14500, 15000, 15500, 16000, 16500 ],
# pulse lists below this line were developed on Steinway D w/ new firmware (11/21/20)
full_pulse20L: [ 1000, 1250, 1500, 1750, 2000, 2250, 2500, 2750, 3000, 3250, 3500, 3750, 4000, 4250, 4500, 4750, 5000, 5250, 5500, 5750, 6000, 6250, 6500, 6750, 7000, 7250, 7500, 7750, 8000, 8250, 8500, 8750, 9000, 9250, 9500, 9750, 10000, 10250, 10500, 10750, 11000, 11250, 11500, 11750, 12000, 12250, 12500, 12750, 13000, 13500, 14000, 14500, 15000, 15500 ],
# 60,72
full_pulse21L: [ 1000, 1250, 1500, 1750, 2000, 2250, 2500, 2750, 3000, 3250, 3500, 3750, 4000, 4500, 5000, 5500, 6000, 6500, 7000, 7500, 8000, 8500, 9000, 10000, 11000, 12000, 13000, 14000, 15000, 16000, 17000, 18000, 19000 ],
# 48,36,85
full_pulse22L: [ 3500, 3750, 4000, 4250, 4500, 4750, 5000, 5250, 5500, 5750, 6000, 6250, 6500, 6750, 7000, 7500, 8000, 8500, 9000, 9500, 10000, 10500, 11000, 12000, 12500, 13000, 14000, 15000, 16000, 17000, 18000, 19000 ],
# 84
full_pulseL: [ 2000, 2250, 2500,2750, 3000, 3250, 3500, 3750, 4000, 4250, 4500, 4750, 5000, 5250, 5500, 5750, 6000, 6250, 6500, 6750, 7000, 7500, 8000, 8500, 9000, 9500, 10000, 10500, 11000, 12000, 12500, 13000, 14000, 15000, 16000 ],
# RMS analysis args
analysisArgs: {
rmsAnalysisArgs: {
@ -77,11 +107,12 @@
durDecayPct: 40, # percent drop in RMS to indicate the end of a note
},
resampleMinDb: 7.0, # note's less than this will be skipped
resampleMinDb: -5.0, # note's less than this will be skipped
resampleNoiseLimitPct: 5.0, #
resampleMinDurMs: 800, # notes's whose duration is less than this will be skipped
resampleMinDurMs: 150, # notes's whose duration is less than this will be skipped
minAttkDb: 7.0, # threshold of silence level
useLastTakeOnlyFl: True,
minAttkDb: -5.0, # threshold of silence level
maxDbOffset: 0.25, # travel down the from the max. note level by at most this amount to locate the max. peak
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
@ -93,6 +124,18 @@
},
manualMinD: {
36: [2, 10],
48: [2, 10],
60: [2, 10],
72: [2, 10],
84: [2, 10]
},
manualAnchorPitchMinDbL: [ 36,48,72,84 ],
manualAnchorPitchMaxDbL: [ 36,48,60,72,84 ],
manualMinD_0: {
23: [2, 24],
24: [2, 18],
25: [2, 41],
@ -175,8 +218,8 @@
},
manualAnchorPitchMinDbL: [ 23, 27, 31, 34, 44, 51, 61, 70, 74, 81, 87, 93, 96, 101 ],
manualAnchorPitchMaxDbL: [ 23, 32, 49, 57, 67, 76, 83, 93, 99, 101 ],
manualAnchorPitchMinDbL_0: [ 23, 27, 31, 34, 44, 51, 61, 70, 74, 81, 87, 93, 96, 101 ],
manualAnchorPitchMaxDbL_0: [ 23, 32, 49, 57, 67, 76, 83, 93, 99, 101 ],
calibrateArgs: {
@ -213,6 +256,89 @@
dbSrcLabel: 'hm', # source of the db measurement 'td' (time-domain) or 'hm' (harmonic)
holdDutyPctD: {
23: [[0, 40]],
24: [[0, 40]],
25: [[0, 40]],
26: [[0, 40]],
27: [[0, 40]],
28: [[0, 40]],
29: [[0, 40]],
30: [[0, 40]],
31: [[0, 40]],
32: [[0, 40]],
33: [[0, 40]],
34: [[0, 40]],
35: [[0, 40]],
36: [[0, 45]],
37: [[0, 40]],
38: [[0, 40]],
39: [[0, 40]],
40: [[0, 40]],
41: [[0, 40]],
42: [[0, 40]],
43: [[0, 40]],
44: [[0, 40]],
45: [[0, 40]],
46: [[0, 40]],
47: [[0, 40]],
48: [[0, 40]],
49: [[0, 40]],
50: [[0, 40]],
51: [[0, 40]],
52: [[0, 40]],
53: [[0, 40]],
54: [[0, 40]],
55: [[0, 40]],
56: [[0, 40]],
57: [[0, 40]],
58: [[0, 40]],
59: [[0, 40]],
60: [[0, 50]],
61: [[0, 43]],
62: [[0, 43]],
63: [[0, 43]],
64: [[0, 40],[14000, 45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
65: [[0, 99]],
66: [[0, 40]],
67: [[0, 40],[14000, 45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
68: [[0, 40],[14000, 45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
69: [[0, 40],[14000, 45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
70: [[0, 40],[14000, 45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
71: [[0, 40],[14000, 45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
72: [[0, 45],[11000, 65] ],
73: [[0, 45],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
74: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
75: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
76: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
77: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
78: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
79: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
80: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
81: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
82: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
83: [[0, 42]],
84: [[0, 42],[10000,50],[11000,60]],
85: [[0, 42],[9000,45],[10000,50],[11000,60]],
86: [[0, 40]],
87: [[0, 40]],
88: [[0, 40]],
89: [[0, 40]],
91: [[0, 40]],
92: [[0, 40]],
93: [[0, 40]],
94: [[0, 40]],
95: [[0, 40]],
96: [[0, 40]],
97: [[0, 40]],
98: [[0, 40]],
99: [[0, 40]],
100: [[0, 40]],
101: [[0, 40]],
106: [[0, 40]]
},
# Final for Matt's piano
holdDutyPct0D: {
23: [[0, 70]],
24: [[0, 75]],
25: [[0, 70]],

View File

@ -1,14 +1,22 @@
##| 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 os, sys,json
import matplotlib.pyplot as plt
import numpy as np
from scipy.io import wavfile
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
import rms_analysis as ra
#from rms_analysis import audio_rms
#from rms_analysis import locate_peak_indexes
#from rms_analysis import audio_stft_rms
#from rms_analysis import calc_harm_bins
def is_nanV( xV ):
for i in range(xV.shape[0]):
@ -31,13 +39,16 @@ def _find_max_take_id( inDir ):
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))
inDir = os.path.join( inDir, str(midi_pitch))
dirL = os.listdir(inDir)
pkL = []
maxTakeNumber = 0
# for each take in this directory
for idir in dirL:
@ -47,6 +58,14 @@ def form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None ):
if not os.path.isfile(os.path.join( inDir,idir, "seq.json")):
continue
if analysisArgsD['useLastTakeOnlyFl']:
if take_number > maxTakeNumber:
pkL.clear()
maxTakeNumber = take_number
else:
continue
# analyze this takes audio and locate the note peaks
r = rms_analysis_main( os.path.join(inDir,idir), midi_pitch, **analysisArgsD['rmsAnalysisArgs'] )
@ -68,6 +87,8 @@ def form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None ):
# locate the first and last note
min_pk_idx, max_pk_idx = find_min_max_peak_index( pkDbL, analysisArgsD['minAttkDb'], analysisArgsD['maxDbOffset'] )
print("MIN MAX:",min_pk_idx,pkUsL[min_pk_idx],max_pk_idx,pkUsL[max_pk_idx])
db1 = pkDbL[ max_pk_idx ]
db0 = pkDbL[ min_pk_idx ]
@ -195,14 +216,12 @@ def calc_resample_ranges( pkDbL, pkUsL, min_pk_idx, max_pk_idx, maxDeltaDb, samp
def form_resample_pulse_time_list( inDir, analysisArgsD ):
def form_resample_pulse_time_list( inDir, midi_pitch, 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] )
inDir = os.path.join( inDir, str(midi_pitch) )
dirL = os.listdir(inDir)
pkL = []
@ -248,11 +267,10 @@ def plot_curve( ax, pulseUsL, rmsDbV ):
ax.plot( pulseUsL, func(pulseUsL), color='red')
def plot_resample_pulse_times_0( inDir, analysisArgsD ):
def plot_resample_pulse_times_0( inDir, analysisArgsD, midi_pitch, printDir="" ):
newPulseUsL, rmsDbV, pulseUsL = form_resample_pulse_time_list( inDir, analysisArgsD )
newPulseUsL, rmsDbV, pulseUsL = form_resample_pulse_time_list( inDir, midi_pitch, analysisArgsD )
midi_pitch = int( inDir.split("/")[-1] )
velTblUsL,velTblDbL,_ = form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None )
fig,ax = plt.subplots()
@ -262,16 +280,18 @@ def plot_resample_pulse_times_0( inDir, analysisArgsD ):
for us in newPulseUsL:
ax.axvline( x = us )
ax.plot(velTblUsL,velTblDbL,marker='.',linestyle='None')
print(len(velTblUsL))
ax.plot(velTblUsL,velTblDbL,marker='.',linestyle='None',color='red')
if printDir:
plt.savefig(os.path.join(printDir,"plot_resample_times_0.png"),format="png")
plt.show()
def plot_resample_pulse_times( inDir, analysisArgsD ):
def plot_resample_pulse_times( inDir, analysisArgsD, midi_pitch, printDir="" ):
newPulseUsL, rmsDbV, pulseUsL = form_resample_pulse_time_list( inDir, analysisArgsD )
newPulseUsL, rmsDbV, pulseUsL = form_resample_pulse_time_list( inDir, midi_pitch, 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]})
@ -290,6 +310,10 @@ def plot_resample_pulse_times( inDir, analysisArgsD ):
axL[1].axhline(1.0,color='black')
axL[1].plot(pulseUsL,np.abs(scoreV * 100.0 / rmsDbV))
axL[1].set_ylim((0.0,50))
if printDir:
plt.savefig(os.path.join(printDir,"plot_resample_times.png"),format="png")
plt.show()
@ -369,7 +393,7 @@ 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 )
harmLBinL,harmMBinL,harmUBinL = ra.calc_harm_bins( srate, binHz, midiPitch, harmN )
fundHz = harmMBinL[0] * binHz
maxPlotHz = fundHz * (harmN+1)
@ -386,19 +410,19 @@ def plot_spectrum( ax, srate, binHz, specV, midiPitch, harmN ):
ax.axvline( x=h1 * binHz, color="black")
ax.axvline( x=h2 * binHz, color="blue")
ax.set_ylabel(str(midiPitch))
ax.set_ylabel("dB : %i " % (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."""
def plot_spectral_ranges( inDir, pitchTakeL, rmsWndMs=300, rmsHopMs=30, harmN=5, dbLinRef=0.001, printDir="" ):
""" Plot the spectrum from one note (7th from last) in each attack pulse length sequence referred to by pitchTakeL."""
plotN = len(pitchL)
plotN = len(pitchTakeL)
fig,axL = plt.subplots(plotN,1)
for plot_idx,midiPitch in enumerate(pitchL):
for plot_idx,(midiPitch,takeId) in enumerate(pitchTakeL):
# 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")
seqFn = os.path.join( inDir, str(midiPitch), str(takeId), "seq.json")
audioFn = os.path.join( inDir, str(midiPitch), str(takeId), "audio.wav")
# read the meta data object
with open( seqFn, "rb") as f:
@ -406,13 +430,20 @@ def plot_spectral_ranges( inDir, pitchL, rmsWndMs=300, rmsHopMs=30, harmN=5, dbL
# read the audio file
srate, signalM = wavfile.read(audioFn)
# convert the audio signal vector to contain only the first (left) channel
if len(signalM.shape)>1:
signalM = signalM[:,0].squeeze()
sigV = signalM / float(0x7fff)
# calc. the RMS envelope in the time domain
rms0DbV, rms0_srate = audio_rms( srate, sigV, rmsWndMs, rmsHopMs, dbLinRef )
rms0DbV, rms0_srate = ra.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'] )
pkIdx0L = ra.locate_peak_indexes( rms0DbV, rms0_srate, r['eventTimeL'] )
# select the 7th to last note for spectrum measurement
@ -423,33 +454,47 @@ def plot_spectral_ranges( inDir, pitchL, rmsWndMs=300, rmsHopMs=30, harmN=5, dbL
# 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)
rmsDbV, rms_srate, specV, specHopIdx, binHz = ra.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 )
axL[-1].set_xlabel("Hertz")
if printDir:
plt.savefig(os.path.join(printDir,"plot_spectral_ranges.png"),format="png")
plt.show()
def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ):
#
r = rms_analysis_main( inDir, midi_pitch, **analysisArgsD['rmsAnalysisArgs'] )
# find min/max peak in the sequence
min_pk_idx, max_pk_idx = find_min_max_peak_index( r.pkDbL, analysisArgsD['minAttkDb'], analysisArgsD['maxDbOffset'] )
# find ranges of the sequence which should be skipped because they are noisy or unreliable
skipPkIdxL = find_skip_peaks( r.rmsDbV, r.pkIdxL, min_pk_idx, max_pk_idx )
# find peaks whose difference to surrounding peaks is greater than 'maxDeltaDb'.
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" )
# plot the harmonic RMS signal
ax.plot( secV, r.rmsDbV, color='blue', label="Harmonic" )
# plot the time-domain RMS signal
ax.plot( np.arange(0,len(r.tdRmsDbV)) / r.rms_srate, r.tdRmsDbV, color="black", label="TD" )
# print beg/end boundaries
# print note beg/end/peak boundaries
for i,(begMs, endMs) in enumerate(r.eventTimeL):
pkSec = r.pkIdxL[i] / r.rms_srate
@ -472,37 +517,44 @@ def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ):
ax.plot( [pki / r.rms_srate], [ r.rmsDbV[pki] ], marker=6, color="blue")
ax.legend();
ax.set_ylabel("dB");
return r
def do_td_plot( inDir, analysisArgs ):
def do_td_plot( inDir, analysisArgs, midi_pitch, takeId, printDir="" ):
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])
# parse the file name
inDir = os.path.join(inDir,str(midi_pitch),str(takeId));
r = td_plot(axL[0],inDir,midi_pitch,id,analysisArgs)
# plot the time domain signal
r = td_plot(axL[0],inDir,midi_pitch,takeId,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].plot( r.pkUsL, r.pkDbL, marker='.', label="harmonic" )
#axL[1].plot( r.pkUsL, qualityV, marker='.', label="quality" )
axL[1].plot( r.pkUsL, avgV, marker='.', label="harm-td avg" )
axL[1].legend()
axL[1].set_ylabel("dB");
#axL[2].plot( r.pkUsL, durMsV, marker='.' )
axL[2].plot( r.pkUsL[1:], dV, marker='.',label='delta')
axL[2].set_ylim([-1,1])
axL[2].legend()
axL[2].set_ylabel("dB");
axL[2].set_xlabel("Microseconds")
sni = select_first_stable_note_by_dur( durMsV )
@ -520,40 +572,62 @@ def do_td_plot( inDir, analysisArgs ):
for i in range(1,len(r.pkUsL)):
axL[2].text( r.pkUsL[i], dV[i-1], "%i" % (i))
if printDir:
plt.savefig(os.path.join(printDir,"do_td_plot.png"),format="png")
plt.show()
def do_td_multi_plot( inDir, analysisArgs ):
def do_td_multi_plot( inDir, analysisArgs, pitchTakeL, printPlotFl=False ):
midi_pitch = int(inDir.split("/")[-1])
#midi_pitch = int(inDir.split("/")[-1])
dirL = os.listdir(inDir)
fig,axL = plt.subplots(len(dirL),1)
fig,axL = plt.subplots(len(pitchTakeL),1)
for id,(idir,ax) in enumerate(zip(dirL,axL)):
for id,((pitch,takeId),ax) in enumerate(zip(pitchTakeL,axL)):
td_plot(ax, os.path.join(inDir,str(id)), midi_pitch, id, analysisArgs )
td_plot(ax, os.path.join(inDir,str(pitch),str(takeId)), pitch, takeId, analysisArgs )
ax.set_xlabel("Seconds")
if printDir:
plt.savefig(os.path.join(printDir,"multi_plot.png"),format="png")
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]
printDir = os.path.expanduser("~/src/picadae_ac_3/doc")
cfgFn = sys.argv[1]
inDir = sys.argv[2]
mode = 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)
if mode == "td_plot":
# python plot_seq.py p_ac.yml ~/temp/p_ac_3_od td_plot 60 10
pitch = int(sys.argv[4])
take_id = int(sys.argv[5])
do_td_plot(inDir,cfg.analysisArgs, pitch, take_id, printDir )
elif mode == "td_multi_plot" or mode == 'plot_spectral_ranges':
pitchTakeIdL = []
for i in range(4,len(sys.argv),2):
pitchTakeIdL.append( (int(sys.argv[i]), int(sys.argv[i+1])) )
if mode == "td_multi_plot":
# python plot_seq.py p_ac.yml ~/temp/p_ac_3_od td_multi_plot 36 2 48 3 60 4
do_td_multi_plot(inDir, cfg.analysisArgs, pitchTakeIdL, printDir )
else:
#do_td_multi_plot(inDir,cfg.analysisArgs)
# python plot_seq.py p_ac.yml ~/temp/p_ac_3_od plot_spectral_ranges 36 2 48 3 60 4
plot_spectral_ranges( inDir, pitchTakeIdL, printDir=printDir )
#plot_spectral_ranges( inDir, [ 24, 36, 48, 60, 72, 84, 96, 104] )
elif mode == "resample_pulse_times":
plot_resample_pulse_times( inDir, cfg.analysisArgs )
# python plot_seq.py p_ac.yml ~/temp/p_ac_3_od resample_pulse_times 60
pitch = int(sys.argv[4])
plot_resample_pulse_times( inDir, cfg.analysisArgs, pitch, printDir )
else:
print("Unknown plot mode:%s" % (mode))

View File

@ -14,6 +14,7 @@ def fit_to_reference( pkL, refTakeId ):
dur_outL = []
tid_outL = []
dbL,usL,durMsL,takeIdL = tuple(zip(*pkL))
us_refL,db_refL,dur_refL = zip(*[(usL[i],dbL[i],durMsL[i]) for i in range(len(usL)) if takeIdL[i]==refTakeId])
@ -27,6 +28,8 @@ def fit_to_reference( pkL, refTakeId ):
db_outL += db0L
else:
db1V = elbow.fit_points_to_reference(us0L,db0L,us_refL,db_refL)
if db1V is not None:
db_outL += db1V.tolist()
us_outL += us0L
@ -44,11 +47,17 @@ def get_merged_pulse_db_measurements( inDir, midi_pitch, analysisArgsD ):
pkL = []
refTakeId = None
usRefL = None
dbRefL = None
# for each take in this directory
for take_number in range(len(takeDirL)):
for take_folder in takeDirL:
take_number = int(take_folder)
if refTakeId is None:
refTakeId = take_number
# analyze this takes audio and locate the note peaks
r = rms_analysis.rms_analysis_main( os.path.join(inDir,str(take_number)), midi_pitch, **analysisArgsD )
@ -57,7 +66,18 @@ def get_merged_pulse_db_measurements( inDir, midi_pitch, analysisArgsD ):
for db,us,stats in zip(r.pkDbL,r.pkUsL,r.statsL):
pkL.append( (db,us,stats.durMs,take_number) )
pkL = fit_to_reference( pkL, 0 )
pkUsL = []
pkDbL = []
durMsL = []
takeIdL = []
holdDutyPctL = []
if refTakeId is None:
print("No valid data files at %s pitch:%i" % (inDir,midi_pitch))
else:
pkL = fit_to_reference( pkL, refTakeId )
# sort the peaks on increasing attack pulse microseconds
pkL = sorted( pkL, key= lambda x: x[1] )
@ -220,6 +240,7 @@ def get_resample_points_wrap( inDir, midi_pitch, analysisArgsD ):
def plot_us_db_curves( ax, inDir, keyMapD, midi_pitch, analysisArgsD, plotResamplePointsFl=False, plotTakesFl=True, usMax=None ):
usL, dbL, durMsL, takeIdL, holdDutyPctL = get_merged_pulse_db_measurements( inDir, midi_pitch, analysisArgsD['rmsAnalysisArgs'] )
reUsL, reDbL, noiseL, resampleL, skipL, firstAudibleIdx, firstNonSkipIdx = get_resample_points( usL, dbL, durMsL, takeIdL, analysisArgsD['resampleMinDurMs'], analysisArgsD['resampleMinDb'], analysisArgsD['resampleNoiseLimitPct'] )
# plot first audible and non-skip position
@ -303,7 +324,7 @@ def plot_us_db_curves( ax, inDir, keyMapD, midi_pitch, analysisArgsD, plotResamp
ax.set_ylabel( "%i %s %s" % (midi_pitch, keyMapD[midi_pitch]['type'],keyMapD[midi_pitch]['class']))
def plot_us_db_curves_main( inDir, cfg, pitchL, plotTakesFl=True, usMax=None ):
def plot_us_db_curves_main( inDir, cfg, pitchL, plotTakesFl=True, usMax=None, printDir="" ):
analysisArgsD = cfg.analysisArgs
keyMapD = { d['midi']:d for d in cfg.key_mapL }
@ -311,6 +332,7 @@ def plot_us_db_curves_main( inDir, cfg, pitchL, plotTakesFl=True, usMax=None ):
fig,axL = plt.subplots(axN,1,sharex=True)
if axN == 1:
axL = [axL]
fig.set_size_inches(18.5, 10.5*axN)
for ax,midi_pitch in zip(axL,pitchL):
@ -319,6 +341,9 @@ def plot_us_db_curves_main( inDir, cfg, pitchL, plotTakesFl=True, usMax=None ):
if plotTakesFl:
plt.legend()
if printDir:
plt.savefig(os.path.join(printDir,"us_db.png"),format="png")
plt.show()
def plot_all_noise_curves( inDir, cfg, pitchL=None ):
@ -332,16 +357,16 @@ def plot_all_noise_curves( inDir, cfg, pitchL=None ):
for midi_pitch in pitchL:
print(midi_pitch)
usL, dbL, durMsL, takeIdL, holdDutyPctL = get_merged_pulse_db_measurements( inDir, midi_pitch, cfg.analysisArgs['rmsAnalysisArgs'] )
scoreV = np.abs( rms_analysis.samples_to_linear_residual( usL, dbL) * 100.0 / dbL )
minDurMs = cfg.analysisArgs['resampleMinDurMs']
minDb = cfg.analysisArgs['resampleMinDb'],
minDb = cfg.analysisArgs['resampleMinDb']
noiseLimitPct = cfg.analysisArgs['resampleNoiseLimitPct']
skipIdxL, firstAudibleIdx, firstNonSkipIdx = get_dur_skip_indexes( durMsL, dbL, scoreV.tolist(), takeIdL, minDurMs, minDb, noiseLimitPct )
@ -364,18 +389,22 @@ def plot_all_noise_curves( inDir, cfg, pitchL=None ):
plt.legend()
plt.show()
def plot_min_max_2_db( inDir, cfg, pitchL=None, takeId=2 ):
def plot_min_max_2_db( inDir, cfg, pitchL=None, takeId=2, printDir=None ):
pitchFolderL = os.listdir(inDir)
print(pitchL)
if pitchL is None:
pitchL = [ int( int(pitchFolder) ) for pitchFolder in pitchFolderL ]
print(pitchL)
okL = []
outPitchL = []
minDbL = []
maxDbL = []
for midi_pitch in pitchL:
print(midi_pitch)
@ -384,12 +413,9 @@ def plot_min_max_2_db( inDir, cfg, pitchL=None, takeId=2 ):
okL.append(False)
takeId = len(set(takeIdL))-1
db_maxL = sorted(dbL)
maxDbL.append( np.mean(db_maxL[-5:]) )
usL,dbL = zip(*[(usL[i],dbL[i]) for i in range(len(usL)) if takeIdL[i]==takeId ])
if len(set(takeIdL)) == 3:
@ -415,9 +441,13 @@ def plot_min_max_2_db( inDir, cfg, pitchL=None, takeId=2 ):
ax.text( pitch, max_db, "%i %s %s" % (pitch, keyMapD[pitch]['type'],keyMapD[pitch]['class']), color=c)
if printDir:
plt.savefig(os.path.join(printDir,"min_max_db_2.png"),format="png")
plt.show()
def plot_min_db_manual( inDir, cfg ):
def plot_min_db_manual( inDir, cfg, printDir=None ):
pitchL = list(cfg.manualMinD.keys())
@ -466,6 +496,8 @@ def plot_min_db_manual( inDir, cfg ):
# Form the complete set of min/max db levels for each pitch by interpolating the
# db values between the manually selected anchor points.
interpMinDbL = np.interp( pitchL, cfg.manualAnchorPitchMinDbL, anchorMinDbL )
@ -493,13 +525,12 @@ def plot_min_db_manual( inDir, cfg ):
if printDir:
plt.savefig(os.path.join(printDir,"manual_db.png"),format="png")
plt.show()
def plot_min_max_db( inDir, cfg, pitchL=None ):
def plot_min_max_db( inDir, cfg, pitchL=None, printDir=None ):
pitchFolderL = os.listdir(inDir)
@ -518,7 +549,7 @@ def plot_min_max_db( inDir, cfg, pitchL=None ):
scoreV = np.abs( rms_analysis.samples_to_linear_residual( usL, dbL) * 100.0 / dbL )
minDurMs = cfg.analysisArgs['resampleMinDurMs']
minDb = cfg.analysisArgs['resampleMinDb'],
minDb = cfg.analysisArgs['resampleMinDb']
noiseLimitPct = cfg.analysisArgs['resampleNoiseLimitPct']
skipIdxL, firstAudibleIdx, firstNonSkipIdx = get_dur_skip_indexes( durMsL, dbL, takeIdL, scoreV.tolist(), minDurMs, minDb, noiseLimitPct )
@ -548,6 +579,9 @@ def plot_min_max_db( inDir, cfg, pitchL=None ):
ax.text( pitch, db, "%i %s %s" % (pitch, keyMapD[pitch]['type'],keyMapD[pitch]['class']))
if printDir:
plt.savefig(os.path.join(printDir,"min_max_db.png"),format="png")
plt.show()
@ -575,7 +609,7 @@ def estimate_us_to_db_map( inDir, cfg, minMapDb=16.0, maxMapDb=26.0, incrMapDb=0
scoreV = np.abs( rms_analysis.samples_to_linear_residual( usL, dbL) * 100.0 / dbL )
minDurMs = cfg.analysisArgs['resampleMinDurMs']
minDb = cfg.analysisArgs['resampleMinDb'],
minDb = cfg.analysisArgs['resampleMinDb']
noiseLimitPct = cfg.analysisArgs['resampleNoiseLimitPct']
# get the set of samples that are not valid (too short, too quiet, too noisy)
@ -633,7 +667,7 @@ def estimate_us_to_db_map( inDir, cfg, minMapDb=16.0, maxMapDb=26.0, incrMapDb=0
return mapD, list(dbS)
def plot_us_to_db_map( inDir, cfg, minMapDb=16.0, maxMapDb=26.0, incrMapDb=1.0, pitchL=None ):
def plot_us_to_db_map( inDir, cfg, minMapDb=16.0, maxMapDb=26.0, incrMapDb=1.0, pitchL=None, printDir=None ):
fig,ax = plt.subplots()
@ -644,6 +678,8 @@ def plot_us_to_db_map( inDir, cfg, minMapDb=16.0, maxMapDb=26.0, incrMapDb=1.0,
u_dL = [ (d['us_avg'],d['us_cls'],d['db_avg'],d['us_std'],d['us_min'],d['us_max'],d['db_std']) for loDb, d in dbD.items() if d['us_avg'] != 0 ]
if u_dL:
# get the us/db lists for this pitch
usL,uscL,dbL,ussL,usnL,usxL,dbsL = zip(*u_dL)
@ -660,6 +696,10 @@ def plot_us_to_db_map( inDir, cfg, minMapDb=16.0, maxMapDb=26.0, incrMapDb=1.0,
plt.legend()
if printDir:
plt.savefig(os.path.join(printDir,"us_db_map.png"),format="png")
plt.show()
def report_take_ids( inDir ):
@ -760,9 +800,11 @@ def gen_vel_map( inDir, cfg, minMaxDbFn, dynLevelN, cacheFn ):
if __name__ == "__main__":
inDir = sys.argv[1]
cfgFn = sys.argv[2]
printDir =os.path.expanduser( "~/src/picadae_ac_3/doc")
cfgFn = sys.argv[1]
inDir = sys.argv[2]
mode = sys.argv[3]
if len(sys.argv) <= 4:
pitchL = None
else:
@ -771,21 +813,23 @@ if __name__ == "__main__":
cfg = parse_yaml_cfg( cfgFn )
if mode == 'us_db':
plot_us_db_curves_main( inDir, cfg, pitchL, plotTakesFl=True,usMax=None )
plot_us_db_curves_main( inDir, cfg, pitchL, plotTakesFl=True,usMax=None, printDir=printDir )
elif mode == 'noise':
plot_all_noise_curves( inDir, cfg, pitchL )
elif mode == 'min_max':
plot_min_max_db( inDir, cfg, pitchL )
plot_min_max_db( inDir, cfg, pitchL, printDir=printDir )
elif mode == 'min_max_2':
plot_min_max_2_db( inDir, cfg, pitchL )
takeId = pitchL[-1]
del pitchL[-1]
plot_min_max_2_db( inDir, cfg, pitchL, takeId=takeId, printDir=printDir )
elif mode == 'us_db_map':
plot_us_to_db_map( inDir, cfg, pitchL=pitchL )
plot_us_to_db_map( inDir, cfg, pitchL=pitchL, printDir=printDir )
elif mode == 'audacity':
rms_analysis.write_audacity_label_files( inDir, cfg.analysisArgs['rmsAnalysisArgs'] )
elif mode == 'rpt_take_ids':
report_take_ids( inDir )
elif mode == 'manual_db':
plot_min_db_manual( inDir, cfg )
plot_min_db_manual( inDir, cfg, printDir=printDir )
elif mode == 'gen_vel_map':
gen_vel_map( inDir, cfg, "minInterpDb.json", 9, "cache_us_db.json" )
elif mode == 'cache_us_db':

View File

@ -201,19 +201,22 @@ def select_first_stable_note_by_delta_db( pkDbL, pkUsL=None, maxPulseUs=0.1 ):
return None
def note_stats( r, decay_pct=50.0, extraDurSearchMs=500 ):
""" Collect some statistics and markers for each note in the sequence. """
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
@ -232,7 +235,15 @@ def note_stats( r, decay_pct=50.0, extraDurSearchMs=500 ):
hmRmsDb_u = 0.0 if ei >= len(r.rmsDbV) else np.mean(r.rmsDbV[bi:ei])
durAvgDb = (hmRmsDb_u + tdRmsDb_u)/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 }))
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 = 0 if qmax <= 0 else statsL[i].quality / qmax
@ -243,15 +254,19 @@ def note_stats( r, decay_pct=50.0, extraDurSearchMs=500 ):
def locate_peak_indexes( xV, xV_srate, eventMsL ):
def locate_peak_indexes( xV, xV_srate, eventMsL, audioFn ):
pkIdxL = []
for begMs, endMs in eventMsL:
for i, (begMs, endMs) in enumerate(eventMsL):
begSmpIdx = int(begMs * xV_srate / 1000.0)
endSmpIdx = int(endMs * xV_srate / 1000.0)
if endSmpIdx != begSmpIdx:
pkIdxL.append( begSmpIdx + np.argmax( xV[begSmpIdx:endSmpIdx] ) )
else:
print("Missing peak %i : begween beg:%i ms end:%i ms : %s" % (i, begMs, endMs, audioFn ))
pkIdxL.append( None )
return pkIdxL
@ -304,6 +319,11 @@ def rms_analyze_one_rt_note_wrap( audioDev, annBegMs, annEndMs, midi_pitch, note
sigV = buf_result.value
if len(sigV.shape) > 1:
sigV = sigV[:,0].squeeze()
# get the annotated begin and end of the note as sample indexes into sigV
bi = int(round(annBegMs * audioDev.srate / 1000))
ei = int(round(annEndMs * audioDev.srate / 1000))
@ -399,47 +419,72 @@ def calibrate_recording_analysis( inDir ):
def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbLinRef=0.001, harmCandN=5, harmN=3, durDecayPct=40 ):
# form the audio and meta data file names
seqFn = os.path.join( inDir, "seq.json")
audioFn = os.path.join( inDir, "audio.wav")
# read the meta data file
with open( seqFn, "rb") as f:
r = json.load(f)
# rad the auido file
srate, signalM = wavfile.read(audioFn)
# convert the audio signal vector to contain only the first (left) channel
if len(signalM.shape)>1:
signalM = signalM[:,0].squeeze()
# convert the auido file to floats in range [-1.0 to 1.0]
sigV = signalM / float(0x7fff)
# calc. the RMS signal
tdRmsDbV, rms0_srate = audio_rms( srate, sigV, rmsWndMs, rmsHopMs, dbLinRef )
tdPkIdxL = locate_peak_indexes( tdRmsDbV, rms0_srate, r['eventTimeL'])
# locate the peaks in the RMS signal
tdPkIdxL = locate_peak_indexes( tdRmsDbV, rms0_srate, r['eventTimeL'], audioFn )
# sum the first harmN harmonics to form a envelope of the audio signal (this is an alternative to the RMS signal)
rmsDbV, rms_srate, binHz = audio_harm_rms( srate, sigV, rmsWndMs, rmsHopMs, dbLinRef, midi_pitch, harmCandN, harmN )
pkIdxL = locate_peak_indexes( rmsDbV, rms_srate, r['eventTimeL'] )
# locate the peaks in the harmonic sum signal
pkIdxL = locate_peak_indexes( rmsDbV, rms_srate, r['eventTimeL'], audioFn )
# form the 'holdDutyPctlL' hold duty cycle transition point list
holdDutyPctL = None
if 'holdDutyPct' in r:
holdDutyPctL = [ (0, r['holdDutyPct']) ]
else:
holdDutyPctL = r['holdDutyPctL']
eventN = len(r['eventTimeL'])
assert( len(tdPkIdxL) == eventN and len(pkIdxL) == eventN )
# filter out all missing events that have no peak
flL = [ (tdPkIdxL[i] is not None) and (pkIdxL[i] is not None) for i in range(eventN) ]
eventTimeL = [ r['eventTimeL'][i] for i in range(eventN) if flL[i] ]
tdPkDbL = [ tdRmsDbV[tdPkIdxL[i]] for i in range(eventN) if flL[i] ]
pkDbL = [ rmsDbV[ pkIdxL[i]] for i in range(eventN) if flL[i] ]
#pkUsL = [ r['pulseUsL'][pkIdxL[i]] for i in range(eventN) if flL[i] ]
tdPkIdxL = [ i for i in tdPkIdxL if i is not None ]
pkIdxL = [ i for i in pkIdxL if i is not None ]
# form the result record
r = types.SimpleNamespace(**{
"audio_srate":srate,
"eventTimeMsL":r['eventTimeL'],
"eventTimeMsL": eventTimeL, #r['eventTimeL'],
"tdRmsDbV": tdRmsDbV,
"tdPkIdxL": tdPkIdxL,
"tdPkDbL": [ tdRmsDbV[i] for i in tdPkIdxL ],
"tdPkDbL": tdPkDbL, # [ tdRmsDbV[i] for i in tdPkIdxL ],
"binHz": binHz,
"rmsDbV":rmsDbV,
"rmsDbV": rmsDbV,
"rms_srate":rms_srate,
"pkIdxL":pkIdxL, # pkIdxL[ len(pulsUsL) ] - indexes into rmsDbV[] of peaks
"eventTimeL":r['eventTimeL'],
"eventTimeL": eventTimeL, #r['eventTimeL'],
"holdDutyPctL":holdDutyPctL,
'pkDbL': [ rmsDbV[ i ] for i in pkIdxL ],
'pkUsL':r['pulseUsL'] })
'pkDbL': pkDbL, # [ rmsDbV[ i ] for i in pkIdxL ],
'pkUsL': r['pulseUsL'] }) # r['pulseUsL']
#
statsL = note_stats(r,durDecayPct)
setattr(r,"statsL", statsL )