Browse Source

Added calibrate.py, plot_calibrate.py and made associated changes.

Added plot_calibrate.ipynb jupyter notebook.
master
kpl 5 years ago
parent
commit
fc1a0d8a61
8 changed files with 1189 additions and 26 deletions
  1. 222
    0
      MidiDevice.py
  2. 427
    0
      calibrate.py
  3. 57
    16
      p_ac.py
  4. 125
    2
      p_ac.yml
  5. 72
    0
      plot_calibrate.ipynb
  6. 150
    0
      plot_calibrate.py
  7. 32
    2
      plot_note_analysis.py
  8. 104
    6
      rms_analysis.py

+ 222
- 0
MidiDevice.py View File

@@ -0,0 +1,222 @@
1
+import rtmidi
2
+import rtmidi.midiutil
3
+
4
+from result import Result
5
+
6
+class MidiDevice(object):
7
+    def __init__(self, **kwargs ):
8
+        self.mip          = None
9
+        self.mop          = None
10
+        self.inMonitorFl  = False
11
+        self.outMonitorFl = False
12
+        self.throughFl    = False
13
+        self.inPortLabel  = None
14
+        self.outPortLabel = None
15
+        self.setup(**kwargs)
16
+
17
+    def setup( self, **kwargs  ):
18
+
19
+        res = Result()
20
+
21
+        if kwargs is None:
22
+            return res
23
+
24
+        if 'inPortLabel' in kwargs:
25
+            res += self.select_port( True, kwargs['inPortLabel'] )
26
+            
27
+        if 'outPortLabel' in kwargs:
28
+            res += self.select_port( False, kwargs['outPortLabel'] )
29
+
30
+        if 'inMonitorFl' in kwargs:
31
+            self.enable_monitor( True, kwargs['inMonitorFl'] )
32
+
33
+        if 'outMonitorFl' in kwargs:
34
+            self.enable_monitor( True, kwargs['outMonitorFl'] )
35
+
36
+        if 'throughFl' in kwargs:
37
+            self.enable_through( kwargs['throughFl'] )
38
+
39
+        return res
40
+
41
+    def _clean_port_label( self, portLabel ):
42
+        return ' '.join(portLabel.split(' ')[:-1])
43
+        
44
+    def _get_port_list( self, inDirFl ):
45
+        dev   =  rtmidi.MidiIn() if inDirFl else rtmidi.MidiOut()
46
+
47
+        # get port list and drop the numeric id at the end of the port label
48
+        return [ self._clean_port_label(p)  for p in dev.get_ports() ]
49
+
50
+        
51
+        
52
+    def get_port_list( self, inDirFl ):
53
+        return { 'type':'midi',
54
+                 'dir': 'in' if inDirFl else 'out',
55
+                 'op':  'list',
56
+                 'listL': self._get_port_list( inDirFl )              
57
+        }
58
+
59
+    def get_in_port_list( self ):
60
+        return self.get_port_list( True )
61
+
62
+    def get_out_port_list( self ):
63
+        return self.get_port_list( False )
64
+    
65
+    def select_port( self, inDirFl, portLabel ):
66
+        res = Result()
67
+        
68
+        if portLabel:
69
+
70
+            dirLabel = "input" if inDirFl else "output"
71
+
72
+            portL = self._get_port_list( inDirFl )
73
+
74
+            if portLabel not in portL:
75
+                res.set_error("The port '%s' is not an available %s port." % (portLabel,dirLabel))
76
+            else:
77
+                port_idx = portL.index(portLabel)  # TODO error check
78
+
79
+                if inDirFl:
80
+                    self.mip,self.inPortLabel = rtmidi.midiutil.open_midiinput(port=port_idx)
81
+                    self.inPortLabel = self._clean_port_label(self.inPortLabel)
82
+                else:
83
+                    self.mop,self.outPortLabel = rtmidi.midiutil.open_midioutput(port=port_idx)
84
+                    self.outPortLabel = self._clean_port_label(self.outPortLabel)
85
+
86
+        return res
87
+
88
+    def select_in_port( self, portLabel ):
89
+        return self.select_port( True, portLabel )
90
+
91
+    def select_out_port( self, portLabel ):
92
+        return self.select_port( False, portLabel )
93
+    
94
+    def enable_through( self, throughFl ):
95
+        self.throughFl = throughFl
96
+
97
+    def enable_monitor( self, inDirFl, monitorFl ):
98
+        if inDirFl:
99
+            self.inMonitorFl = monitorFl
100
+        else:
101
+            self.outMonitorFl = monitorFl
102
+
103
+    def enable_in_monitor( self, monitorFl):
104
+        self.enable_monitor( True, monitorFl )
105
+        
106
+    def enable_out_monitor( self, monitorFl):
107
+        self.enable_monitor( False, monitorFl )
108
+
109
+    def port_name( self, inDirFl ):
110
+        return inPortLabel if inDirFl else outPortLabel
111
+
112
+    def in_port_name( self ):
113
+        return self.port_name(True)
114
+
115
+    def out_port_name( self ):
116
+        return self.port_name(False)
117
+
118
+    def _midi_data_to_text_msg( self, inFl, midi_data ):
119
+
120
+        text = ""
121
+        if len(midi_data) > 0:
122
+            text += "{:02x}".format(midi_data[0])
123
+            
124
+        if len(midi_data) > 1:
125
+            text += " {:3d}".format(midi_data[1])
126
+
127
+        if len(midi_data) > 2:
128
+            text += " {:3d}".format(midi_data[2])
129
+
130
+        text = ("in:  " if inFl else "out: ") + text
131
+        return { 'type':'midi', 'dir':inFl, 'op':'monitor', 'value':text }
132
+    
133
+    def get_input( self ):
134
+        o_msgL = []
135
+        
136
+        if self.mip is not None:
137
+            midi_msg = self.mip.get_message()
138
+            if midi_msg and midi_msg[0]:
139
+                
140
+                if self.monitorInFl:
141
+                    o_msgL.append( self._midi_data_to_text_msg(True,midi_msg[0]) )
142
+                    
143
+                if self.throughFl and self.mop is not None:
144
+                    self.mop.send_message(midi_msg[0])
145
+                                   
146
+                    
147
+                o_msgL.append( { 'type':'midi', 'op':'data', 'dir':'in', 'value':midi_msg[0] } )
148
+                    
149
+        return o_msgL
150
+        
151
+
152
+    def send_output( self, m ):
153
+        o_msgL = []
154
+        
155
+        if self.mop is not None:
156
+            self.mop.send_message(m)
157
+                                   
158
+        if self.outMonitorFl:
159
+            o_msgL += [self._midi_data_to_text_msg( False, m )]
160
+                                   
161
+        return o_msgL                           
162
+
163
+    def send_note_on( self, pitch, vel, ch=0 ):
164
+        return self.send_output( [ 0x90+ch, pitch, vel ] )
165
+
166
+    def send_note_off( self, pitch, ch=0 ):
167
+        return self.send_note_on( 0, ch )
168
+
169
+    def send_pgm_change( self, pgm, ch=0 ):
170
+        return self.send_output( [ 0xc0+ch, pgm ] )
171
+
172
+    def send_pbend( self, val, ch=0 ):
173
+        assert( val < 8192 )
174
+
175
+        ival = int(val)
176
+
177
+        lsb = ival & 0x7f
178
+        msb = (ival >> 7) & 0x7f
179
+
180
+        return self.send_output( [ 0xe0+ch, lsb, msb ] )
181
+        
182
+
183
+    def send_controller( self, num, value, ch=0 ):
184
+        return self.send_output( [0xb0+ch, num, value ] )
185
+
186
+    def send_all_notes_off(self, ch=0 ):
187
+        return self.send_controller( 123, 1, ch=ch )
188
+
189
+    def get_state( self ):
190
+        return  {
191
+            "inMonitorFl":self.inMonitorFl,
192
+            "outMonitorFl":self.outMonitorFl,
193
+            "throughFl":self.throughFl,
194
+            "inPortLabel":self.inPortLabel,
195
+            "outPortLabel":self.outPortLabel,
196
+            "inPortL":self.get_in_port_list(),
197
+            "outPortL":self.get_out_port_list()
198
+        }
199
+        
200
+        
201
+    def on_command( self, m, ms ):
202
+        errL = []
203
+        
204
+        if m.type == 'midi':
205
+            if m.op == 'sel':
206
+                errL.append(self.select_port( m.dir=='in', m.value ))
207
+                
208
+            elif m.op == 'through':
209
+                self.enable_through( m.value )
210
+                
211
+            elif m.op == 'monitor':
212
+                self.enable_monitor( m.dir=='in', m.value )
213
+
214
+        return errL
215
+
216
+if __name__ == "__main__":
217
+
218
+    md = MidiDevice()
219
+
220
+    print(md.get_port_list( True ))
221
+    print(md.get_port_list( False ))
222
+    

+ 427
- 0
calibrate.py View File

@@ -0,0 +1,427 @@
1
+import os,types,wave,json,array
2
+import numpy as np
3
+from rms_analysis import rms_analyze_one_note
4
+
5
+class Calibrate:
6
+    def __init__( self, cfg, audio, midi, api ):
7
+        self.cfg   = types.SimpleNamespace(**cfg)
8
+        self.audio = audio
9
+        self.midi  = midi
10
+        self.api   = api
11
+        self.state = "stopped"  # stopped | started | note_on | note_off | analyzing
12
+        self.playOnlyFl = False
13
+        self.startMs = None
14
+        self.nextStateChangeMs = None
15
+        self.curHoldDutyCyclePctD = None  # { pitch:dutyPct}
16
+        self.noteAnnotationL = []  # (noteOnMs,noteOffMs,pitch,pulseUs)
17
+
18
+        self.measD = None   # { midi_pitch: [ {pulseUs, db, durMs, targetDb } ] }
19
+
20
+        self.curNoteStartMs = None
21
+        self.curPitchIdx = None
22
+        self.curTargetDbIdx = None
23
+        self.successN = None
24
+        self.failN    = None
25
+
26
+        self.curTargetDb = None
27
+        self.curPulseUs = None
28
+        self.curMatchN = None
29
+        self.curAttemptN = None
30
+        self.lastAudiblePulseUs = None
31
+        self.maxTooShortPulseUs = None
32
+        self.pulseDbL = None
33
+        self.deltaUpMult = None
34
+        self.deltaDnMult = None
35
+        self.skipMeasFl  = None
36
+        
37
+    def start(self,ms):
38
+        self.stop(ms)
39
+        self.state             = 'started'
40
+        self.playOnlyFl        = False
41
+        self.nextStateChangeMs = ms + 500
42
+        
43
+        self.startMs           = ms
44
+
45
+        self.curPitchIdx    = 0
46
+        self.curPulseUs     = self.cfg.initPulseUs
47
+        self.lastAudiblePulseUs = None
48
+        self.maxTooShortPulseUs = None
49
+        self.pulseDbL = []
50
+        self.deltaUpMult = 1
51
+        self.deltaDnMult = 1
52
+        self.curTargetDbIdx = -1       
53
+        self._start_new_db_target()
54
+        
55
+        self.curDutyPctD = {}
56
+        self.skipMeasFl = False
57
+        self.measD = {}
58
+        
59
+        self.successN = 0
60
+        self.failN = 0
61
+        self.audio.record_enable(True)
62
+
63
+    def stop(self,ms):
64
+
65
+        if self.midi is not None:
66
+            self.midi.send_all_notes_off()
67
+
68
+        if not self.playOnlyFl:
69
+            self.audio.record_enable(False)
70
+
71
+            self._save_results()
72
+
73
+    def play(self,ms):
74
+
75
+        if self.measD is None or len(self.measD) == 0:
76
+            print("Nothing to play.")
77
+        else:
78
+            self.state             = 'started'
79
+            self.playOnlyFl        = True
80
+            self.nextStateChangeMs = ms + 500
81
+            self.curPitchIdx       = -1
82
+            self.curTargetDbIdx    = 0
83
+            self._do_play_update()
84
+        
85
+    def tick(self,ms):
86
+
87
+        if self.nextStateChangeMs is not None and ms > self.nextStateChangeMs:
88
+                    
89
+            if self.state == 'stopped':
90
+                pass
91
+
92
+            elif self.state == 'started':
93
+                self._do_note_on(ms)
94
+                self.nextStateChangeMs += self.cfg.noteOnDurMs
95
+                self.state = 'note_on'
96
+
97
+            elif self.state == 'note_on':
98
+                self._do_note_off(ms)
99
+                self.nextStateChangeMs += self.cfg.noteOffDurMs
100
+                self.state = 'note_off'
101
+
102
+            elif self.state == 'note_off':
103
+                if self.playOnlyFl:
104
+                    if not self._do_play_update():
105
+                        self.state = 'stopped'
106
+                else:
107
+                    if self._do_analysis(ms):
108
+                        if not self._start_new_db_target():
109
+                            self.stop(ms)
110
+                            self.state = 'stopped'
111
+                            print("DONE!")
112
+
113
+                # if the state was not changed to 'stopped'
114
+                if self.state == 'note_off':
115
+                    self.state = 'started'
116
+                    
117
+                
118
+    def _do_play_update( self ):
119
+
120
+        self.curPitchIdx +=1
121
+        if self.curPitchIdx >= len(self.cfg.pitchL):
122
+            self.curPitchIdx = 0
123
+            self.curTargetDbIdx += 1
124
+            if self.curTargetDbIdx >= len(self.cfg.targetDbL):
125
+                return False
126
+
127
+        pitch = self.cfg.pitchL[ self.curPitchIdx ]
128
+        targetDb = self.cfg.targetDbL[ self.curTargetDbIdx ]
129
+        self.curPulseUs = -1
130
+        for d in self.measD[ pitch ]:
131
+            if d['targetDb'] == targetDb and d['matchFl']==True:
132
+                self.curPulseUs = d['pulse_us']
133
+                break
134
+
135
+        if self.curPulseUs == -1:
136
+            print("Pitch:%i TargetDb:%f not found." % (pitch,targetDb))
137
+            return False
138
+
139
+        print("Target db: %4.1f" % (targetDb))
140
+        
141
+        return True
142
+            
143
+    
144
+        
145
+    def _get_duty_cycle( self, pitch, pulseUsec ):
146
+        
147
+        dutyPct = 50
148
+
149
+        if pitch in self.cfg.holdDutyPctD:
150
+            
151
+            dutyPct = self.cfg.holdDutyPctD[pitch][0][1]
152
+            for refUsec,refDuty in self.cfg.holdDutyPctD[pitch]:
153
+                if pulseUsec < refUsec:
154
+                    break
155
+                dutyPct = refDuty
156
+
157
+        return dutyPct
158
+            
159
+    def _set_duty_cycle( self, pitch, pulseUsec ):
160
+
161
+        dutyPct = self._get_duty_cycle( pitch, pulseUsec )
162
+
163
+        if pitch not in self.curDutyPctD or self.curDutyPctD[pitch] != dutyPct:
164
+            self.curDutyPctD[pitch] = dutyPct
165
+            self.api.set_pwm_duty( pitch, dutyPct )
166
+            print("Hold Duty Set:",dutyPct)
167
+            self.skipMeasFl = True
168
+
169
+        return dutyPct
170
+    
171
+    def _do_note_on(self,ms):
172
+        self.curNoteStartMs = ms
173
+
174
+        pitch = self.cfg.pitchL[ self.curPitchIdx]
175
+        
176
+        if self.midi is not None:
177
+            self.midi.send_note_on( pitch, 60 )
178
+        else:
179
+            self._set_duty_cycle( pitch, self.curPulseUs )
180
+            self.api.note_on_us( pitch, self.curPulseUs )
181
+
182
+
183
+        print("note-on:  ",pitch," ",self.curPulseUs," us")
184
+        
185
+    def _do_note_off(self,ms):
186
+        self.noteAnnotationL.append( { 'beg_ms':self.curNoteStartMs-self.startMs, 'end_ms':ms-self.startMs, 'midi_pitch':self.cfg.pitchL[ self.curPitchIdx], 'pulse_us':self.curPulseUs } )
187
+
188
+        if self.midi is not None:
189
+            self.midi.send_note_off( self.cfg.pitchL[ self.curPitchIdx] )
190
+        else:
191
+            for pitch in self.cfg.pitchL:
192
+                self.api.note_off( pitch )
193
+
194
+            
195
+        #print("note-off: ",self.cfg.pitchL[ self.curPitchIdx])
196
+
197
+
198
+    def _calc_next_pulse_us( self, targetDb ):
199
+
200
+        # sort pulseDb ascending on db
201
+        self.pulseDbL = sorted( self.pulseDbL, key=lambda x: x[1] )
202
+
203
+        
204
+        pulseL,dbL = zip(*self.pulseDbL)
205
+
206
+        max_i = np.argmax(dbL)
207
+        min_i = np.argmin(dbL)        
208
+
209
+        if targetDb > dbL[max_i]:
210
+            pu =  pulseL[max_i] + self.deltaUpMult * 500
211
+            self.deltaUpMult += 1
212
+
213
+        elif targetDb < dbL[min_i]:
214
+            pu = pulseL[min_i] - self.deltaDnMult * 500
215
+            self.deltaDnMult += 1
216
+            if self.maxTooShortPulseUs is not None and pu < self.maxTooShortPulseUs:
217
+                # BUG: this is a problem is self.pulseL[min_i] is <= than self.maxTooShortPulseUs
218
+                # the abs() covers the problem to prevent decreasing from maxTooShortPulseus
219
+                pu = self.maxTooShortPulseUs + (abs(pulseL[min_i] - self.maxTooShortPulseUs))/2
220
+                self.deltaDnMult = 1                
221
+        else:
222
+            self.deltaUpMult = 1
223
+            self.deltaDnMult = 1
224
+            pu =  np.interp([targetDb],dbL,pulseL)
225
+
226
+        return max(min(pu,self.cfg.maxPulseUs),self.cfg.minPulseUs)
227
+    
228
+    def _do_analysis(self,ms):
229
+
230
+        analysisDoneFl = False
231
+        midi_pitch     = self.cfg.pitchL[self.curPitchIdx]
232
+        pulse_us       = self.curPulseUs
233
+        
234
+        measD = self._meas_note(midi_pitch,pulse_us)
235
+
236
+        # if the the 'skip' flag is set then don't analyze this note
237
+        if self.skipMeasFl:
238
+            self.skipMeasFl = False
239
+            print("SKIP")
240
+        else:
241
+
242
+            db    = measD[self.cfg.dbSrcLabel]['db']
243
+            durMs = measD['hm']['durMs']
244
+
245
+            # if this note is shorter than the minimum allowable duration
246
+            if durMs < self.cfg.minMeasDurMs:
247
+
248
+                print("SHORT!")
249
+                
250
+                if self.maxTooShortPulseUs is None or self.curPulseUs > self.maxTooShortPulseUs:
251
+                    self.maxTooShortPulseUs = self.curPulseUs
252
+
253
+                if self.lastAudiblePulseUs is not None and self.curPulseUs < self.lastAudiblePulseUs:
254
+                    self.curPulseUs = self.lastAudiblePulseUs
255
+                else:
256
+                    self.curPulseUs = self.cfg.initPulseUs
257
+                
258
+            else:
259
+
260
+                # this is a valid measurement store it to the pulse-db table 
261
+                self.pulseDbL.append( (self.curPulseUs,db) )
262
+
263
+                # track the most recent audible note - to return to if a successive note is too short
264
+                self.lastAudiblePulseUs = self.curPulseUs
265
+                
266
+                # calc the upper and lower bounds db range
267
+                lwr_db = self.curTargetDb * ((100.0 - self.cfg.tolDbPct)/100.0)
268
+                upr_db = self.curTargetDb * ((100.0 + self.cfg.tolDbPct)/100.0)
269
+
270
+                # was this note is inside the db range then set the 'match' flag
271
+                if lwr_db <= db and db <= upr_db:
272
+                    self.curMatchN += 1
273
+                    measD['matchFl'] = True
274
+                    print("MATCH!")
275
+
276
+                # 
277
+                self.curPulseUs = int(self._calc_next_pulse_us(self.curTargetDb))
278
+
279
+            # if at least minMatchN matches have been made on this pitch/targetDb 
280
+            if self.curMatchN >= self.cfg.minMatchN:
281
+                analysisDoneFl = True
282
+                self.successN += 1
283
+                print("Anysis Done: Success")
284
+
285
+            # if at least maxAttemptN match attempts have been made without success
286
+            self.curAttemptN += 1
287
+            if self.curAttemptN >= self.cfg.maxAttemptN:
288
+                analysisDoneFl = True
289
+                self.failN += 1
290
+                print("Analysis Done: Fail")
291
+
292
+                
293
+        if midi_pitch not in self.measD:
294
+            self.measD[ midi_pitch ] = []
295
+
296
+        self.measD[ midi_pitch ].append( measD )
297
+
298
+        return analysisDoneFl
299
+    
300
+
301
+    def _meas_note(self,midi_pitch,pulse_us):
302
+        
303
+        # get the annotation information for the last note
304
+        annD = self.noteAnnotationL[-1]
305
+
306
+        buf_result = self.audio.linear_buffer()
307
+
308
+        if buf_result:
309
+
310
+            sigV = buf_result.value
311
+
312
+            # get the annotated begin and end of the note as sample indexes into sigV
313
+            bi = int(round(annD['beg_ms'] * self.audio.srate / 1000))
314
+            ei = int(round(annD['end_ms'] * self.audio.srate / 1000))
315
+
316
+            # calculate half the length of the note-off duration in samples
317
+            noteOffSmp_o_2 = int(round(self.cfg.noteOffDurMs/2  * self.audio.srate / 1000))
318
+
319
+            # widen the note analysis space noteOffSmp_o_2 samples pre/post the annotated begin/end of the note
320
+            bi = max(0,bi - noteOffSmp_o_2)
321
+            ei = min(noteOffSmp_o_2,sigV.shape[0]-1)
322
+
323
+            
324
+            ar = types.SimpleNamespace(**self.cfg.analysisD)
325
+
326
+            # shift the annotatd begin/end of the note to be relative to  index bi
327
+            begMs = noteOffSmp_o_2 * 1000 / self.audio.srate
328
+            endMs = begMs + (annD['end_ms'] - annD['beg_ms'])
329
+
330
+            # analyze the note
331
+            resD  = rms_analyze_rt_one_note( sigV[bi:ei], self.audio.srate, begMs, endMs, midi_pitch, rmsWndMs=ar.rmsWndMs, rmsHopMs=ar.rmsHopMs, dbRefWndMs=ar.dbRefWndMs, harmCandN=ar.harmCandN, harmN=ar.harmN, durDecayPct=ar.durDecayPct )
332
+
333
+            resD["pulse_us"]   = pulse_us 
334
+            resD["midi_pitch"] = midi_pitch
335
+            resD["beg_ms"]     = annD['beg_ms']
336
+            resD['end_ms']     = annD['end_ms']
337
+            resD['skipMeasFl'] = self.skipMeasFl
338
+            resD['matchFl']    = False
339
+            resD['targetDb']   = self.curTargetDb
340
+            resD['annIdx']     = len(self.noteAnnotationL)-1
341
+            
342
+            print( "%4.1f hm:%4.1f (%4.1f) %4i  td:%4.1f (%4.1f) %4i" %  (self.curTargetDb,resD['hm']['db'], resD['hm']['db']-self.curTargetDb, resD['hm']['durMs'], resD['td']['db'], resD['td']['db']-self.curTargetDb, resD['td']['durMs']))
343
+
344
+        
345
+        return resD
346
+
347
+    
348
+
349
+    def _start_new_db_target(self):
350
+        
351
+        self.curTargetDbIdx += 1
352
+        
353
+        # if all db targets have been queried then advance to the next pitch
354
+        if self.curTargetDbIdx >= len(self.cfg.targetDbL):
355
+            
356
+            self.curTargetDbIdx = 0
357
+            self.curPitchIdx += 1
358
+
359
+            # if all pitches have been queried then we are done
360
+            if self.curPitchIdx >= len(self.cfg.pitchL):
361
+                return False
362
+
363
+            
364
+        self.curTargetDb        = self.cfg.targetDbL[ self.curTargetDbIdx ]
365
+        self.curMatchN          = 0
366
+        self.curAttemptN        = 0
367
+        self.lastAudiblePulseUs = None
368
+        self.maxTooShortPulseUs = None
369
+        self.pulseDbL           = []
370
+        self.deltaUpMult        = 1
371
+        self.deltaDnMult        = 1
372
+        return True
373
+
374
+
375
+    def _write_16_bit_wav_file( self, fn ):
376
+
377
+        srate = int(self.audio.srate)
378
+
379
+        buf_result = self.audio.linear_buffer()
380
+
381
+        sigV = buf_result.value
382
+
383
+        smpN = sigV.shape[0]
384
+        chN  = 1
385
+        sigV = np.squeeze(sigV.reshape( smpN * chN, )) * 0x7fff
386
+        sigL = [ int(round(sigV[i])) for i in range(smpN) ]
387
+
388
+        sigA = array.array('h',sigL)
389
+
390
+        with wave.open( fn, "wb") as f:
391
+
392
+            bits = 16
393
+            bits_per_byte = 8
394
+            f.setparams((chN, bits//bits_per_byte, srate, 0, 'NONE', 'not compressed'))
395
+
396
+            f.writeframes(sigA)
397
+
398
+    def _save_results( self ):
399
+
400
+        if self.measD is None or len(self.measD) == 0:
401
+            return
402
+        
403
+        outDir = os.path.expanduser( self.cfg.outDir )
404
+        
405
+        if not os.path.isdir(outDir):
406
+            os.mkdir(outDir)
407
+
408
+        outDir = os.path.join( outDir, self.cfg.outLabel )
409
+
410
+        if not os.path.isdir(outDir):
411
+            os.mkdir(outDir)
412
+
413
+        i = 0
414
+        while( os.path.isdir( os.path.join(outDir,"%i" % i )) ):
415
+            i += 1
416
+
417
+        outDir = os.path.join( outDir, "%i" % i )
418
+        os.mkdir(outDir)
419
+
420
+        self._write_16_bit_wav_file( os.path.join(outDir,"audio.wav"))
421
+
422
+        d = {'cfg':self.cfg.__dict__, 'measD': self.measD, 'annoteL':self.noteAnnotationL }
423
+
424
+        with open( os.path.join(outDir,"meas.json"), "w") as f:
425
+            json.dump(d,f)
426
+
427
+            

+ 57
- 16
p_ac.py View File

@@ -6,12 +6,14 @@ from multiprocessing import Process, Pipe
6 6
 
7 7
 from picadae_api  import Picadae
8 8
 from AudioDevice  import AudioDevice
9
+from MidiDevice   import MidiDevice
9 10
 from result       import Result
10 11
 from common       import parse_yaml_cfg
11 12
 from plot_seq     import form_resample_pulse_time_list
12 13
 from plot_seq     import form_final_pulse_list
13 14
 from rt_note_analysis import RT_Analyzer
14 15
 from keyboard         import Keyboard
16
+from calibrate        import Calibrate
15 17
 
16 18
 class AttackPulseSeq:
17 19
     """ Sequence a fixed chord over a list of attack pulse lengths."""
@@ -274,13 +276,15 @@ class App:
274 276
         self.cfg       = None
275 277
         self.audioDev  = None
276 278
         self.api       = None
277
-        self.calibrate = None
279
+        self.cal_keys  = None
278 280
         self.keyboard  = None
281
+        self.calibrate = None
279 282
         
280 283
     def setup( self, cfg ):
281 284
         self.cfg = cfg
282 285
 
283 286
         self.audioDev = AudioDevice()
287
+        self.midiDev = MidiDevice()
284 288
 
285 289
         #
286 290
         # TODO: unify the result error handling
@@ -292,20 +296,31 @@ class App:
292 296
         if not res:
293 297
             self.audio_dev_list(0)
294 298
         else:
295
-            self.api = Picadae( key_mapL=cfg.key_mapL)
296 299
 
297
-            # wait for the letter 'a' to come back from the serial port
298
-            api_res = self.api.wait_for_serial_sync(timeoutMs=cfg.serial_sync_timeout_ms)
300
+            if hasattr(cfg,'midi'):
301
+                res = self.midiDev.setup(**cfg.midi)
299 302
 
300
-            # did the serial port sync fail?
301
-            if not api_res:
302
-                res.set_error("Serial port sync failed.")
303
+                if not res:
304
+                    self.midi_dev_list(0)
303 305
             else:
304
-                print("Serial port sync'ed")
306
+                self.midiDev = None
307
+
308
+                self.api = Picadae( key_mapL=cfg.key_mapL)
309
+
310
+                # wait for the letter 'a' to come back from the serial port
311
+                api_res = self.api.wait_for_serial_sync(timeoutMs=cfg.serial_sync_timeout_ms)
312
+
313
+                # did the serial port sync fail?
314
+                if not api_res:
315
+                    res.set_error("Serial port sync failed.")
316
+                else:
317
+                    print("Serial port sync'ed")
305 318
 
306
-                self.calibrate = CalibrateKeys( cfg, self.audioDev, self.api )
319
+                    self.cal_keys = CalibrateKeys( cfg, self.audioDev, self.api )
307 320
 
308
-                self.keyboard  = Keyboard( cfg, self.audioDev, self.api )
321
+                    self.keyboard  = Keyboard( cfg, self.audioDev, self.api )
322
+
323
+                    self.calibrate = Calibrate( cfg.calibrateArgs, self.audioDev, self.midiDev, self.api )
309 324
     
310 325
         return res
311 326
 
@@ -313,10 +328,14 @@ class App:
313 328
         
314 329
         self.audioDev.tick(ms)
315 330
         
316
-        if self.calibrate:
317
-            self.calibrate.tick(ms)
331
+        if self.cal_keys:
332
+            self.cal_keys.tick(ms)
333
+            
318 334
         if self.keyboard:
319 335
             self.keyboard.tick(ms)
336
+            
337
+        if self.calibrate:
338
+            self.calibrate.tick(ms)
320 339
 
321 340
     def audio_dev_list( self, ms ):
322 341
         portL = self.audioDev.get_port_list( True )
@@ -324,13 +343,25 @@ class App:
324 343
         for port in portL:
325 344
             print("chs:%4i label:%s" % (port['chN'],port['label']))
326 345
 
346
+    def midi_dev_list( self, ms ):
347
+        d = self.midiDev.get_port_list( True )
348
+
349
+        for port in d['listL']:
350
+            print("IN:",port)
351
+        
352
+        d = self.midiDev.get_port_list( False )
353
+          
354
+        for port in d['listL']:
355
+            print("OUT:",port)
356
+        
357
+
327 358
     def calibrate_keys_start( self, ms, pitchRangeL ):
328 359
         chordL = [ [pitch]  for pitch in range(pitchRangeL[0], pitchRangeL[1]+1)]
329
-        self.calibrate.start(  ms, chordL, cfg.full_pulseL )
360
+        self.cal_keys.start(  ms, chordL, cfg.full_pulseL )
330 361
 
331 362
     def play_keys_start( self, ms, pitchRangeL ):
332 363
         chordL = [ [pitch]  for pitch in range(pitchRangeL[0], pitchRangeL[1]+1)]
333
-        self.calibrate.start(  ms, chordL, cfg.full_pulseL, playOnlyFl=True )
364
+        self.cal_keys.start(  ms, chordL, cfg.full_pulseL, playOnlyFl=True )
334 365
 
335 366
     def keyboard_start_pulse_idx( self, ms, argL ):
336 367
         pitchL = [ pitch  for pitch in range(argL[0], argL[1]+1)]        
@@ -346,9 +377,16 @@ class App:
346 377
     def keyboard_repeat_target_db( self, ms, argL ):
347 378
         self.keyboard.repeat(  ms, None, argL[0] )
348 379
 
380
+    def calibrate_start( self, ms, argL ):
381
+        self.calibrate.start(ms)
382
+        
383
+    def calibrate_play( self, ms, argL ):
384
+        self.calibrate.play(ms)
385
+        
349 386
     def calibrate_keys_stop( self, ms ):
350
-        self.calibrate.stop(ms)
387
+        self.cal_keys.stop(ms)
351 388
         self.keyboard.stop(ms)
389
+        self.calibrate.stop(ms)
352 390
         
353 391
     def quit( self, ms ):
354 392
         if self.api:
@@ -424,7 +462,7 @@ def app_event_loop_func( pipe, cfg ):
424 462
 
425 463
 
426 464
         # give some time to the system
427
-        time.sleep(0.1)
465
+        time.sleep(0.05)
428 466
         
429 467
         # calc the tick() time stamp
430 468
         ms  = int(round( (datetime.now() - dt0).total_seconds() * 1000.0) )
@@ -470,7 +508,10 @@ class Shell:
470 508
             'q':{ "func":'quit',                      "minN":0,  "maxN":0, "help":"quit"},
471 509
             '?':{ "func":"_help",                     "minN":0,  "maxN":0, "help":"Print usage text."},
472 510
             'a':{ "func":"audio_dev_list",            "minN":0,  "maxN":0, "help":"List the audio devices."},
511
+            'm':{ "func":"midi_dev_list",             "minN":0,  "maxN":0, "help":"List the MIDI devices."},
473 512
             'c':{ "func":"calibrate_keys_start",      "minN":1,  "maxN":2, "help":"Calibrate a range of keys. "},
513
+            'd':{ "func":"calibrate_start",           "minN":1,  "maxN":1, "help":"Calibrate based on fixed db levels. "},
514
+            'D':{ "func":"calibrate_play",            "minN":1,  "maxN":1, "help":"Play back last calibration."},
474 515
             's':{ "func":"calibrate_keys_stop",       "minN":0,  "maxN":0, "help":"Stop key calibration"},
475 516
             'p':{ "func":"play_keys_start",           "minN":1,  "maxN":2, "help":"Play current calibration"},
476 517
             'k':{ "func":"keyboard_start_pulse_idx",  "minN":3,  "maxN":3, "help":"Play pulse index across keyboard"},

+ 125
- 2
p_ac.yml View File

@@ -6,8 +6,17 @@
6 6
     audio: {
7 7
       inPortLabel: "5 USB Audio CODEC:", #"HDA Intel PCH: CS4208", # "5 USB Audio CODEC:", #"5 USB Sound Device",
8 8
       outPortLabel: ,
9
-      },
9
+    },
10 10
 
11
+    midi_off: {
12
+        inMonitorFl: False,
13
+        outMonitorFl: False,
14
+        throughFl: False,
15
+        inPortLabel: "Fastlane:Fastlane MIDI A",
16
+        outPortLabel: "Fastlane:Fastlane MIDI A"
17
+        #inPortLabel: "picadae:picadae MIDI 1",
18
+        #outPortLabel: "picadae:picadae MIDI 1"
19
+    },
11 20
     
12 21
     # Picadae API args
13 22
     serial_dev: "/dev/ttyACM0",
@@ -61,6 +70,121 @@
61 70
       finalPulseListCacheFn: "/home/kevin/temp/final_pulse_list_cache.pickle",
62 71
       rmsAnalysisCacheFn: "/home/kevin/temp/rms_analysis_cache.pickle"
63 72
       },
73
+
74
+
75
+      calibrateArgs: {
76
+
77
+        outDir: "~/temp/calib0",
78
+        outLabel: "test",
79
+        
80
+        analysisD: {
81
+          rmsWndMs: 300,    # length of the RMS measurment window
82
+          rmsHopMs: 30,     # RMS measurement inter window distance
83
+          dbRefWndMs: 500,  # length of initial portion of signal to use to calculate the dB reference level
84
+          harmCandN: 5,     # count of harmonic candidates to locate during harmonic based RMS analysis
85
+          harmN: 3,         # count of harmonics to use to calculate harmonic based RMS analysis
86
+          durDecayPct: 40   # percent drop in RMS to indicate the end of a note
87
+          },
88
+    
89
+          noteOnDurMs: 1000,
90
+          noteOffDurMs: 1000,
91
+    
92
+          
93
+          pitchL: [ 50, 51, 52 ],                    # list of pitches
94
+          targetDbL: [ 16, 20, 23 ],                  # list of target db
95
+
96
+          minMeasDurMs: 800,             # minimum candidate note duration
97
+          tolDbPct: 5.0,                 # tolerance as a percent of targetDb above/below used to form match db window
98
+          maxPulseUs: 45000,             # max. allowable pulse us
99
+          minPulseUs:  8000,             # min. allowable pulse us
100
+          initPulseUs: 15000,            # pulseUs for first note
101
+          minMatchN: 3,                  # at least 3 candidate notes must be within tolDbPct to move on to a new targetDb
102
+          maxAttemptN: 30,               # give up if more than 20 candidate notes fail for a given targetDb
103
+          dbSrcLabel: 'td',              # source of the db measurement 'td' (time-domain) or 'hm' (harmonic)
104
+
105
+          holdDutyPctD:  {
106
+          23: [[0, 70]],
107
+          24: [[0, 75]],
108
+          25: [[0, 70]],
109
+          26: [[0, 65]],
110
+          27: [[0, 70]],
111
+          28: [[0, 70]],
112
+          29: [[0, 65]],
113
+          30: [[0, 65]],
114
+          31: [[0, 65]],
115
+          32: [[0, 60]],
116
+          33: [[0, 65]],
117
+          34: [[0, 65]],
118
+          35: [[0, 65]],
119
+          36: [[0, 65]],
120
+          37: [[0, 65]],
121
+          38: [[0, 60]],
122
+          39: [[0, 60]],
123
+          40: [[0, 55]],
124
+          41: [[0, 60]],
125
+          42: [[0, 60]],
126
+          43: [[0, 65]],
127
+          44: [[0, 60]],
128
+          45: [[0, 60]],
129
+          46: [[0, 60]],
130
+          47: [[0, 60]],
131
+          48: [[0, 70]],
132
+          49: [[0, 60]],
133
+          50: [[0, 50]],
134
+          51: [[0, 50]],
135
+          52: [[0, 55]],
136
+          53: [[0, 50]],
137
+          54: [[0, 50]],
138
+          55: [[0, 50], [22000, 55]],
139
+          56: [[0, 50]],
140
+          57: [[0, 50]],
141
+          58: [[0, 50]],
142
+          59: [[0, 60]],
143
+          60: [[0, 50]],
144
+          61: [[0, 50]],
145
+          62: [[0, 55]],
146
+          63: [[0, 50]],
147
+          64: [[0, 50]],
148
+          65: [[0, 50], [17000, 65]],
149
+          66: [[0, 53]],
150
+          67: [[0, 55]],
151
+          68: [[0, 53]],
152
+          69: [[0, 55]],
153
+          70: [[0, 50]],
154
+          71: [[0, 50]],
155
+          72: [[0, 60]],
156
+          73: [[0, 50]],
157
+          74: [[0, 60]],
158
+          75: [[0, 55]],
159
+          76: [[0, 70]],
160
+          77: [[0, 50], [15000, 60], [19000, 70]],
161
+          78: [[0, 60]],
162
+          79: [[0, 50], [15000, 60], [19000, 70]],
163
+          80: [[0, 45]],
164
+          81: [[0, 50], [15000, 70]],
165
+          82: [[0, 50], [12500, 60], [14000, 65], [17000, 70]],
166
+          83: [[0, 50], [15000, 65]],
167
+          84: [[0, 50], [12500, 60], [14000, 65], [17000, 70]],
168
+          85: [[0, 50], [12500, 60], [14000, 65], [17000, 70]],
169
+          86: [[0, 50], [12500, 60], [14000, 65], [17000, 70]],
170
+          87: [[0, 50], [14000, 60]],
171
+          88: [[0, 50], [12500, 60], [14000, 65], [17000, 70]],
172
+          89: [[0, 50], [12500, 60], [14000, 65], [17000, 70]],
173
+          91: [[0, 40], [12500, 50], [14000, 60], [17000, 65]],
174
+          92: [[0, 40], [14000, 50]],
175
+          93: [[0, 40], [12500, 50], [14000, 60], [17000, 65]],
176
+          94: [[0, 40], [14000, 50]],
177
+          95: [[0, 40], [12500, 50], [14000, 60], [17000, 65]],
178
+          96: [[0, 40], [12500, 50], [14000, 60], [17000, 65]],
179
+          97: [[0, 40], [14000, 50]],
180
+          98: [[0, 50]],
181
+          99: [[0, 50]],
182
+          100: [[0, 50]],
183
+          101: [[0, 50]],
184
+          106: [[0, 50]]
185
+          },
186
+          
187
+        },
64 188
     
65 189
      key_mapL: [
66 190
 
@@ -163,6 +287,5 @@
163 287
        ]
164 288
       
165 289
     
166
-    
167 290
     }
168 291
 }

+ 72
- 0
plot_calibrate.ipynb
File diff suppressed because it is too large
View File


+ 150
- 0
plot_calibrate.py View File

@@ -0,0 +1,150 @@
1
+import sys,os,json,types
2
+import numpy as np
3
+import matplotlib.pyplot as plt
4
+import matplotlib._color_data as mcd
5
+from matplotlib.pyplot import figure
6
+
7
+from rms_analysis import calibrate_recording_analysis
8
+
9
+def plot_by_pitch( inDir, pitch=None ):
10
+
11
+    anlD = calibrate_recording_analysis( inDir )
12
+    jsonFn  = os.path.join(inDir, "meas.json" )
13
+    audioFn = os.path.join(inDir, "audio.wav" )
14
+
15
+    with open(jsonFn,"r") as f:
16
+        r = json.load(f)
17
+
18
+    measD = r['measD']
19
+    cfg  = types.SimpleNamespace(**r['cfg'])
20
+    
21
+    axN = len(measD) if pitch is None else 1
22
+    fig,axL = plt.subplots(axN,1)
23
+    fig.set_size_inches(18.5, 10.5*axN)
24
+
25
+
26
+    # for each pitch
27
+    for axi,(midi_pitch,measL)in enumerate(measD.items()):
28
+
29
+        midi_pitch = int(midi_pitch)
30
+        
31
+        if pitch is not None and pitch != midi_pitch:
32
+            continue
33
+
34
+        if pitch is not None:
35
+            axi = 0
36
+            axL = [ axL ]
37
+        
38
+        targetDbS  = set()
39
+        hmPulseDbL = []
40
+        tdPulseDbL = []
41
+        anPulseDbL = []
42
+
43
+        # for each measurement on this pitch
44
+        for mi,d in enumerate(measL):
45
+            m = types.SimpleNamespace(**d)
46
+
47
+            # form a list of pulse/db measurements associated with this pitch
48
+            hmPulseDbL.append( (m.pulse_us,m.hm['db'],m.matchFl,m.hm['durMs'],m.skipMeasFl) )
49
+            tdPulseDbL.append( (m.pulse_us,m.td['db'],m.matchFl,m.td['durMs'],m.skipMeasFl) )
50
+
51
+            ar = next(ad for ad in anlD[midi_pitch] if ad['meas_idx']==mi )
52
+            anPulseDbL.append( (m.pulse_us,ar['db'],m.matchFl,m.hm['durMs'],m.skipMeasFl))
53
+
54
+            # get the unique set of targets
55
+            targetDbS.add(m.targetDb)
56
+
57
+
58
+        # sort measurements on pulse length
59
+        hmPulseDbL = sorted(hmPulseDbL,key=lambda x: x[0])
60
+        tdPulseDbL = sorted(tdPulseDbL,key=lambda x: x[0])
61
+        anPulseDbL = sorted(anPulseDbL,key=lambda x: x[0])
62
+
63
+        # plot the re-analysis 
64
+        pulseL,dbL,matchFlL,_,_ = zip(*anPulseDbL)
65
+        axL[axi].plot( pulseL, dbL, label="post", marker='.' )
66
+        
67
+        # plot harmonic measurements
68
+        pulseL,dbL,matchFlL,durMsL,skipFlL = zip(*hmPulseDbL)
69
+        axL[axi].plot( pulseL, dbL, label="harm", marker='.' )
70
+
71
+        # plot time-domain based measuremented
72
+        pulseL,dbL,matchFlL,_,_ = zip(*tdPulseDbL)
73
+        axL[axi].plot( pulseL, dbL, label="td", marker='.' )
74
+
75
+        
76
+        # plot target boundaries
77
+        for targetDb in targetDbS:
78
+            lwr = targetDb * ((100.0 - cfg.tolDbPct)/100.0)
79
+            upr = targetDb * ((100.0 + cfg.tolDbPct)/100.0 )       
80
+
81
+            axL[axi].axhline(targetDb)
82
+            axL[axi].axhline(lwr,color='lightgray')
83
+            axL[axi].axhline(upr,color='gray')
84
+
85
+        # plot match and 'too-short' markers
86
+        for i,matchFl in enumerate(matchFlL):
87
+
88
+            if durMsL[i] < cfg.minMeasDurMs:
89
+                axL[axi].plot( pulseL[i], dbL[i], marker='x', color='black', linestyle='None')
90
+
91
+            if skipFlL[i]:
92
+                axL[axi].plot( pulseL[i], dbL[i], marker='+', color='blue', linestyle='None')
93
+                
94
+            if matchFl:
95
+                axL[axi].plot( pulseL[i], dbL[i], marker='.', color='red', linestyle='None')
96
+
97
+                
98
+                
99
+
100
+        axL[axi].set_title("pitch:%i " % (midi_pitch))
101
+        
102
+    plt.legend()
103
+    plt.show()
104
+
105
+def plot_all_notes( inDir ):
106
+
107
+    jsonFn  = os.path.join(inDir, "meas.json" )
108
+    audioFn = os.path.join(inDir, "audio.wav" )
109
+
110
+    with open(jsonFn,"r") as f:
111
+        r = json.load(f)
112
+
113
+    measD = r['measD']
114
+
115
+    axN = 0
116
+    for midi_pitch,measL in measD.items():
117
+        axN += len(measL)
118
+
119
+    print(axN)
120
+    fig,axL = plt.subplots(axN,1)
121
+    fig.set_size_inches(18.5, 10.5*axN)
122
+    
123
+
124
+    i = 0
125
+    for midi_pitch,measL in measD.items():
126
+        for d in measL:
127
+            axL[i].plot(d['td']['rmsDbV'])
128
+            axL[i].plot(d['hm']['rmsDbV'])
129
+
130
+            axL[i].axvline(d['td']['pk_idx'],color='red')
131
+            axL[i].axvline(d['hm']['pk_idx'],color='green')
132
+
133
+            i += 1
134
+
135
+    plt.show()
136
+
137
+
138
+
139
+if __name__ == "__main__":
140
+
141
+    pitch = None
142
+    inDir = sys.argv[1]
143
+    if len(sys.argv) > 2:
144
+        pitch = int(sys.argv[2])
145
+
146
+    #plot_all_notes( inDir )
147
+    plot_by_pitch(inDir,pitch)
148
+    #calibrate_recording_analysis( inDir )
149
+    
150
+    

+ 32
- 2
plot_note_analysis.py View File

@@ -1,4 +1,4 @@
1
-import os,sys,pickle
1
+import os,sys,pickle,json
2 2
 import numpy as np
3 3
 
4 4
 import matplotlib.pyplot as plt
@@ -279,9 +279,37 @@ def plot_quiet_note_db( cacheFn, yamlCfgFn, minDurMs=700 ):
279 279
     plt.show()
280 280
     
281 281
     
282
+def dump_hold_duty_pct( inDir ):
283
+
284
+    pitchL = []
285
+    folderL = os.listdir(inDir)
286
+
287
+    for folder in folderL:
288
+
289
+        midi_pitch = int(folder)
290
+        
291
+        fn = os.path.join( inDir,folder,"0","seq.json")
282 292
         
293
+        if not os.path.isfile(fn):
294
+            print("No sequence file:%s" % (fn))
295
+        else:
296
+            with open(fn,"r") as f:
297
+                d = json.load(f)
298
+
299
+                if 'holdDutyPct' in d:
300
+                    holdDutyPctL = [ [0,d['holdDutyPct']] ]
301
+                else:
302
+                    holdDutyPctL = d['holdDutyPctL']
283 303
 
304
+                pitchL.append( {'pitch':midi_pitch, 'holdDutyPctL':holdDutyPctL} )
305
+                #print(midi_pitch, holdDutyPctL)
284 306
         
307
+    pitchL = sorted(pitchL, key=lambda x:  x['pitch'])
308
+
309
+    for d in pitchL:
310
+        print("{",d['pitch'],":",d['holdDutyPctL'],"},")
311
+                    
312
+                
285 313
 if __name__ == "__main__":
286 314
 
287 315
     #inDir = sys.argv[1]
@@ -297,4 +325,6 @@ if __name__ == "__main__":
297 325
     #get_all_note_durations("/home/kevin/temp/p_ac_3c",durFn)
298 326
     #plot_all_note_durations(durFn, np.arange(45,55),2,"p_ac.yml",800,20000)
299 327
 
300
-    plot_quiet_note_db(durFn,"p_ac.yml")
328
+    #plot_quiet_note_db(durFn,"p_ac.yml")
329
+
330
+    dump_hold_duty_pct( "/home/kevin/temp/p_ac_3c" )

+ 104
- 6
rms_analysis.py View File

@@ -1,4 +1,4 @@
1
-import os,types,json,pickle
1
+import os,types,json,pickle,types
2 2
 from scipy.io import wavfile
3 3
 from scipy.signal import stft
4 4
 import numpy as np
@@ -27,8 +27,9 @@ def calc_harm_bins( srate, binHz, midiPitch, harmN ):
27 27
     return fund_l_binL, fund_m_binL, fund_u_binL
28 28
     
29 29
 def rms_to_db( xV, rms_srate, refWndMs ):
30
-    dbWndN = int(round(refWndMs * rms_srate / 1000.0))
31
-    dbRef = ref = np.mean(xV[0:dbWndN])
30
+    #dbWndN = int(round(refWndMs * rms_srate / 1000.0))
31
+    #dbRef = ref = np.mean(xV[0:dbWndN])
32
+    dbRef = refWndMs   ######################################################### HACK HACK HACK HACK HACK
32 33
     rmsDbV = 20.0 * np.log10( xV / dbRef )
33 34
 
34 35
     return rmsDbV
@@ -213,9 +214,6 @@ def note_stats( r, decay_pct=50.0, extraDurSearchMs=500 ):
213 214
         bi = pkSmpIdx
214 215
         ei = pkSmpIdx + int(round(durMs * srate / 1000.0))
215 216
 
216
-        #bi = begSmpIdx
217
-        #ei = endSmpIdx
218
-
219 217
         qualityCoeff =  np.sum(r.rmsDbV[bi:ei]) + np.sum(r.tdRmsDbV[bi:ei])
220 218
         if qualityCoeff > qmax:
221 219
             qmax = qualityCoeff
@@ -259,6 +257,106 @@ def key_info_dictionary( keyMapL=None, yamlCfgFn=None):
259 257
 
260 258
     return kmD
261 259
 
260
+def rms_analyze_one_rt_note( sigV, srate, begMs, endMs, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=500, harmCandN=5, harmN=3, durDecayPct=40 ):
261
+
262
+    sigV = np.squeeze(sigV)
263
+
264
+                        # HACK HACK HACK HACK
265
+    dbRefWndMs = 0.002  # HACK HACK HACK HACK
266
+                        # HACK HACK HACK HACK
267
+    td_rmsDbV, td_srate = audio_rms( srate, sigV, rmsWndMs, rmsHopMs, dbRefWndMs )
268
+
269
+    begSmpIdx = int(round(begMs * td_srate/1000))
270
+    endSmpIdx = int(round(endMs * td_srate/1000))
271
+    td_pk_idx = begSmpIdx + np.argmax(td_rmsDbV[begSmpIdx:endSmpIdx])
272
+
273
+    td_durMs = measure_duration_ms( td_rmsDbV, td_srate, td_pk_idx, len(sigV)-1, durDecayPct )
274
+
275
+                       # HACK HACK HACK HACK    
276
+    dbRefWndMs = 0.01  # HACK HACK HACK HACK
277
+                       # HACK HACK HACK HACK
278
+     
279
+    hm_rmsDbV, hm_srate, binHz = audio_harm_rms( srate, sigV, rmsWndMs, rmsHopMs, dbRefWndMs, midi_pitch, harmCandN, harmN  )
280
+
281
+    begSmpIdx = int(round(begMs * hm_srate/1000))
282
+    endSmpIdx = int(round(endMs * hm_srate/1000))
283
+    hm_pk_idx = begSmpIdx + np.argmax(hm_rmsDbV[begSmpIdx:endSmpIdx])
284
+
285
+    hm_durMs = measure_duration_ms( hm_rmsDbV, hm_srate, hm_pk_idx, len(sigV)-1, durDecayPct )
286
+
287
+    tdD = { "rmsDbV":td_rmsDbV.tolist(), "srate":td_srate, "pk_idx":int(td_pk_idx), "db":float(td_rmsDbV[td_pk_idx]), "durMs":td_durMs }
288
+    hmD = { "rmsDbV":hm_rmsDbV.tolist(), "srate":hm_srate, "pk_idx":int(hm_pk_idx), "db":float(hm_rmsDbV[hm_pk_idx]), "durMs":hm_durMs }
289
+    
290
+    return { "td":tdD, "hm":hmD }
291
+
292
+def calibrate_rms( sigV, srate, beg_ms, end_ms ):
293
+
294
+    bi = int(round(beg_ms * srate / 1000))
295
+    ei = int(round(end_ms * srate / 1000))
296
+    rms = np.sqrt( np.mean( sigV[bi:ei] * sigV[bi:ei] ))
297
+
298
+    return 20.0*np.log10( rms / 0.002 )
299
+    
300
+
301
+def calibrate_recording_analysis( inDir ):
302
+
303
+    jsonFn  = os.path.join(inDir, "meas.json" )
304
+    audioFn = os.path.join(inDir, "audio.wav" )
305
+
306
+    with open(jsonFn,"r") as f:
307
+        r = json.load(f)
308
+
309
+    measD = r['measD']
310
+    cfg   = types.SimpleNamespace(**r['cfg'])
311
+    annL  = r['annoteL']
312
+    anlD  = {}
313
+    
314
+    n = 0
315
+    for midi_pitch,measL in measD.items():
316
+        n += len(measL)
317
+        anlD[int(midi_pitch)] = []
318
+
319
+    srate, signalM  = wavfile.read(audioFn)
320
+    sigV  = signalM / float(0x7fff)
321
+
322
+    anlr = types.SimpleNamespace(**cfg.analysisD)
323
+
324
+                        # HACK HACK HACK HACK
325
+    dbRefWndMs = 0.002  # HACK HACK HACK HACK
326
+                        # HACK HACK HACK HACK
327
+    
328
+    tdRmsDbV, td_srate = audio_rms( srate, sigV, anlr.rmsWndMs, anlr.rmsHopMs, dbRefWndMs )
329
+
330
+    # for each measured pitch 
331
+    for midi_pitch,measL in measD.items():
332
+
333
+        # for each measured note at this pitch
334
+        for mi,d in enumerate(measL):
335
+
336
+            mr = types.SimpleNamespace(**d)
337
+            
338
+            # locate the associated annotation reocrd
339
+            for annD in annL:
340
+                ar = types.SimpleNamespace(**annD)
341
+
342
+                if ar.midi_pitch == mr.midi_pitch and ar.beg_ms==mr.beg_ms and ar.end_ms==mr.end_ms:
343
+                    assert( ar.pulse_us == mr.pulse_us )
344
+
345
+                    bi = int(round(ar.beg_ms * td_srate / 1000))
346
+                    ei = int(round(ar.end_ms * td_srate / 1000))
347
+                    db = np.mean(tdRmsDbV[bi:ei])
348
+
349
+                    db = calibrate_rms( sigV, srate, ar.beg_ms, ar.end_ms )
350
+                    
351
+                    anlD[int(midi_pitch)].append({ 'pulse_us':ar.pulse_us, 'db':db, 'meas_idx':mi })
352
+
353
+                    break
354
+
355
+
356
+    return anlD
357
+                    
358
+        
359
+
262 360
 
263 361
 def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=500, harmCandN=5, harmN=3, durDecayPct=40 ):
264 362
 

Loading…
Cancel
Save