Browse Source

ChordTester.py,NoteTester.py,PolyNoteTester.py,VelTablePlayer.py : iniital commit.

master
kevin 3 years ago
parent
commit
ca030abcf5
4 changed files with 483 additions and 0 deletions
  1. 68
    0
      ChordTester.py
  2. 119
    0
      NoteTester.py
  3. 148
    0
      PolyNoteTester.py
  4. 148
    0
      VelTablePlayer.py

+ 68
- 0
ChordTester.py View File

@@ -0,0 +1,68 @@
1
+import types
2
+import time
3
+
4
+class ChordTester:
5
+    def __init__( self, cfg, api ):
6
+        self.api = api
7
+        self.cfg = types.SimpleNamespace(**cfg.ChordTester)
8
+
9
+        self.nextMs       = 0
10
+        self.isStartedFl  = False
11
+        self.curNoteCnt   = 0
12
+        self.curRepeatCnt = 0
13
+        self.isPlayingFl  = False
14
+
15
+
16
+    def start( self ):
17
+        self.api.set_hold_duty_all( self.cfg.holdDuty )
18
+        if self.cfg.useVelTableFl:
19
+            self.api.set_vel_table_all( self.cfg.pitchL )
20
+        self.curNoteCnt = 0
21
+        self.curRepeatCnt = 0
22
+        self.isStartedFl = True
23
+
24
+    def stop( self ):
25
+        self.isStartedFl = False
26
+        self.api.all_notes_off()
27
+        
28
+
29
+
30
+    def tick( self, ms ):
31
+
32
+        if self.isStartedFl and ms >= self.nextMs:
33
+
34
+            if self.isPlayingFl:
35
+
36
+                # turn notes off
37
+                for i in range(0,self.curNoteCnt+1):
38
+                    self.api.note_off( self.cfg.pitchL[i])
39
+                    time.sleep( 0.01 )
40
+
41
+                # repeat or advance the chord note count
42
+                self.curRepeatCnt += 1
43
+                if self.curRepeatCnt >= self.cfg.repeatCnt:
44
+                    self.curRepeatCnt = 0
45
+                    self.curNoteCnt  += 1
46
+                    if self.curNoteCnt >= len(self.cfg.pitchL):
47
+                        self.isStartedFl = False
48
+                        self.curNoteCnt = 0
49
+                        
50
+                self.isPlayingFl = False
51
+                self.nextMs      = ms + self.cfg.pauseMs
52
+                
53
+            else:
54
+                for i in range(0,self.curNoteCnt+1):
55
+                    if self.cfg.useVelTableFl:
56
+                        self.api.note_on_vel(self.cfg.pitchL[i], 45 )
57
+                    else:
58
+                        self.api.note_on_us(self.cfg.pitchL[i], self.cfg.atkUsec)
59
+                    time.sleep( 0.02 )
60
+                    
61
+                self.nextMs = ms + self.cfg.durMs
62
+                self.isPlayingFl = True
63
+                    
64
+                    
65
+                    
66
+
67
+        
68
+            

+ 119
- 0
NoteTester.py View File

@@ -0,0 +1,119 @@
1
+import sys,os,types,json
2
+from random import randrange
3
+
4
+class NoteTester:
5
+    def __init__( self, cfg, api  ):
6
+        self.cfg = cfg
7
+        self.api = api
8
+
9
+        r = types.SimpleNamespace(**cfg.NoteTester)
10
+        
11
+        self.durMsL   = [ randrange(r.minNoteDurMs,  r.maxNoteDurMs)  for _ in range(r.noteCount) ]
12
+        self.pauseMsL = [ randrange(r.minPauseDurMs, r.maxPauseDurMs) for _ in range(r.noteCount) ]
13
+        self.eventL   = []
14
+        self.nextMs   = 0        # next transition time
15
+        self.eventIdx = 0        # next event to play
16
+        self.noteOnFl = False    # True if note is currently sounding
17
+        self.pitch    = r.pitch  #
18
+        self.filename    = r.filename
19
+        self.isStartedFl = False
20
+        self.minAttackUsec = r.minAttackUsec
21
+        self.maxAttackUsec = r.maxAttackUsec
22
+    
23
+    def start( self ):
24
+        self.eventIdx = 0
25
+        self.noteOnFl = False
26
+        self.nextMs   = 0
27
+        self.isStartedFl = True
28
+
29
+    def stop( self ):
30
+        self.isStartedFl = False
31
+        self.write()
32
+
33
+    def tick( self, ms ):
34
+
35
+        if self.isStartedFl and ms > self.nextMs:
36
+
37
+            offsMs = 0
38
+            
39
+            if self.noteOnFl:
40
+                self.noteOnFl = False
41
+                self.api.note_off( self.pitch )                
42
+                offsMs = self.pauseMsL[ self.eventIdx ]
43
+                self.eventIdx += 1
44
+                print("off:%i ms" % (offsMs))
45
+                    
46
+
47
+            else:
48
+                usec        = self.minAttackUsec + (int(self.eventIdx * 250) % int(self.maxAttackUsec - self.minAttackUsec))
49
+                decay_level = self.api.calc_decay_level( usec )
50
+
51
+                self.api.note_on_us( self.pitch, usec, decay_level )
52
+                offsMs = self.durMsL[ self.eventIdx ]
53
+                print("usec:%i %i dcy:%i" % (usec,offsMs, decay_level) )
54
+                self.noteOnFl = True
55
+                
56
+
57
+            self.eventL.append( (ms, self.noteOnFl) )
58
+            self.nextMs = ms + offsMs
59
+    
60
+            if self.eventIdx >= len(self.durMsL):
61
+                self.write();
62
+                self.isStartedFl = False
63
+                print("done % i" % (len(self.eventL)))
64
+
65
+        
66
+        
67
+    def write( self ):
68
+
69
+        with open(self.filename,"w") as f:
70
+            json.dump({ "eventL":self.eventL },f )
71
+
72
+
73
+
74
+def note_tester_compare( nt_fn, logica_fn ):
75
+
76
+    eventL  = []
77
+    logicaL = []
78
+
79
+    with open(nt_fn,"r") as f:
80
+        r = json.load(f)
81
+        eventL =  r['eventL']
82
+        eventL = [ (ms-eventL[0][0], level ) for ms,level in eventL  ]
83
+
84
+    with open(logica_fn,"r") as f:
85
+        logicaL = [ ( d['count']/16e3,d['level'])  for d in json.load(f) if d['signal'] == 0 ]
86
+        logicaL = [ (ms-logicaL[0][0], level!=0 ) for ms,level in logicaL ]
87
+
88
+
89
+    print(len(eventL))
90
+    print(len(logicaL))
91
+
92
+    #edL = [ eventL[i][0]  - eventL[i-1][0]  for i in range(2,len(eventL)) ]
93
+    #ldL = [ logicaL[i][0] - logicaL[i-1][0] for i in range(2,len(logicaL)) ]
94
+
95
+    #print(edL[:10])
96
+    #print(ldL[:10])
97
+
98
+
99
+    durMs = 0
100
+    ms = 0
101
+    for i,(t0,t1) in enumerate(zip(eventL,logicaL)):
102
+        t = t0[0]                              # eventL[] time
103
+        dt = int(t - t1[0])                    # diff between eventL[] and logicaL[] time
104
+        fl = ' ' if t0[1] == t1[1] else '*'    # mark level mismatch with '*'
105
+        print("%5i %7i %4i %i %s" % (i,durMs,dt,t0[1],fl))
106
+        durMs = t-ms                             
107
+        ms = t
108
+        
109
+    
110
+if __name__ == "__main__":
111
+
112
+    nt_fn = "note_tester.json"
113
+    logica_fn = sys.argv[1]
114
+    if len(sys.argv) > 2:
115
+        nt_fn     = sys.argv[2]
116
+
117
+    note_tester_compare( nt_fn, logica_fn)
118
+        
119
+    

+ 148
- 0
PolyNoteTester.py View File

@@ -0,0 +1,148 @@
1
+import types
2
+import time
3
+from random import randrange
4
+
5
+class PolyNoteTester:
6
+    def __init__( self, cfg, api  ):
7
+        self.api = api
8
+
9
+        r = types.SimpleNamespace(**cfg.PolyNoteTester)
10
+        self.cfg = r
11
+
12
+        if r.mode == "simple":
13
+            print("mode:simple")
14
+            self.schedL = self._gen_simple_sched(r)
15
+        else:
16
+            print("mode:poly")
17
+            self.schedL = self._gen_sched(r)
18
+
19
+            
20
+        self.schedL = sorted( self.schedL, key=lambda x: x[0] )    
21
+        self.nextMs   = 0        # next transition time
22
+        self.schedIdx = 0        # next event to play
23
+        self.isStartedFl   = False
24
+
25
+        #self._report()
26
+
27
+    def _report( self ):
28
+        for t,cmd,pitch,atkUs in self.schedL:
29
+            print("%s %6i %3i %5i" % (cmd,t,pitch,atkUs))
30
+
31
+    def _gen_simple_sched( self, r ):
32
+        """ Play each note sequentially from lowest to highest. """
33
+        durMs  = int(r.minNoteDurMs + (r.maxNoteDurMs - r.minNoteDurMs)/2)
34
+        ioMs   = int(r.minInterOnsetMs + (r.maxInterOnsetMs - r.minInterOnsetMs)/2)
35
+        atkUs  = int(r.minAttackUsec + (r.maxAttackUsec - r.minAttackUsec)/2)
36
+        schedL = []
37
+
38
+        t0 = 0
39
+        for pitch in range(r.minPitch,r.maxPitch+1):
40
+            schedL.append((t0,'on',pitch,atkUs))
41
+            schedL.append((t0+durMs,'off',pitch,0))
42
+            t0 += durMs + ioMs
43
+
44
+        return schedL
45
+
46
+        
47
+    def _does_note_overlap( self, beg0Ms, end0Ms, beg1Ms, end1Ms ):
48
+        """ if note 0 is entirely before or after note 1 """
49
+        return not (beg0Ms > end1Ms or end0Ms < beg1Ms)
50
+
51
+    def _do_any_notes_overlap( self, begMs, endMs, begEndL ):
52
+        for beg1,end1 in begEndL:
53
+            if self._does_note_overlap(begMs,endMs,beg1,end1):
54
+                return True
55
+        return False
56
+
57
+    def _get_last_end_time( self, begEndL ):
58
+        end0 = 0
59
+        for beg,end in begEndL:
60
+            if end > end0:
61
+                end0 = end
62
+
63
+        return end0
64
+            
65
+                    
66
+    def _gen_sched( self, r ):
67
+
68
+        pitchL = [ randrange(r.minPitch,r.maxPitch) for _ in range(r.noteCount) ]
69
+        durMsL = [ randrange(r.minNoteDurMs, r.maxNoteDurMs) for _ in range(r.noteCount) ]
70
+        ioMsL  = [ randrange(r.minInterOnsetMs, r.maxInterOnsetMs) for _ in range(r.noteCount) ]
71
+        atkUsL = [ randrange(r.minAttackUsec,   r.maxAttackUsec)   for _ in range(r.noteCount) ]
72
+        schedL = []
73
+        pitchD = {} # pitch: [ (begMs,endMs) ]
74
+
75
+        t0 = 0
76
+        # for each pitch,dur,ioi,atkUs tuple
77
+        for pitch,durMs,interOnsetMs,atkUs in zip(pitchL,durMsL,ioMsL,atkUsL):
78
+
79
+            # calc note begin and end time
80
+            begMs = t0
81
+            endMs = t0 + durMs
82
+
83
+            # if this pitch hasn't yet been added to pitchD
84
+            if pitch not in pitchD:
85
+                pitchD[ pitch ] = [ (begMs,endMs) ]
86
+            else:
87
+
88
+                # if the proposed note overlaps with other notes for this pitch
89
+                if self._do_any_notes_overlap( begMs, endMs, pitchD[pitch] ):
90
+                    # move this pitch past the last note
91
+                    begMs = self._get_last_end_time( pitchD[pitch] ) + interOnsetMs
92
+                    endMs = begMs + durMs
93
+
94
+                # add the new note to pitchD
95
+                pitchD[ pitch ].append( (begMs,endMs) )
96
+
97
+            
98
+            # update the schedule
99
+            schedL.append( (begMs, 'on', pitch, atkUs)) 
100
+            schedL.append( (endMs, 'off', pitch, 0 ))
101
+            
102
+            t0 += interOnsetMs
103
+
104
+        
105
+        return schedL
106
+        
107
+        
108
+    def start( self ):
109
+        self.schedIdx = 0
110
+        self.nextMs   = 0
111
+        self.api.set_hold_duty_all(self.cfg.holdDutyPct)
112
+        self.isStartedFl = True
113
+        
114
+
115
+    def stop( self ):
116
+        self.isStartedFl = False
117
+        self.api.all_notes_off()
118
+        
119
+
120
+    def tick( self, ms ):
121
+
122
+        while self.isStartedFl and ms >= self.nextMs and self.schedIdx < len(self.schedL):
123
+
124
+            t0,cmd,pitch,usec = self.schedL[self.schedIdx]
125
+
126
+            if cmd == 'on':
127
+                if pitch not in self.cfg.skipPitchL:
128
+                    decay_level = self.api.calc_decay_level( usec )
129
+                    self.api.note_on_us( pitch, usec, decay_level )
130
+                    print("on  %i %i %i" % (pitch,usec,decay_level))
131
+                
132
+            elif cmd == 'off':
133
+                if pitch not in self.cfg.skipPitchL:
134
+                    self.api.note_off( pitch )
135
+                    print("off %i" % pitch)
136
+            
137
+            self.schedIdx += 1
138
+            if self.schedIdx < len(self.schedL):
139
+                self.nextMs = ms + (self.schedL[self.schedIdx][0] - t0)
140
+            else:
141
+                self.isStartedFl = False
142
+                print("Done.")
143
+        
144
+        
145
+
146
+
147
+
148
+    

+ 148
- 0
VelTablePlayer.py View File

@@ -0,0 +1,148 @@
1
+import json
2
+from rt_note_analysis import RT_Analyzer
3
+
4
+class VelTablePlayer:
5
+    
6
+    def __init__( self, cfg, api, audio, holdDutyPctD, fn  ):
7
+        self.cfg          = cfg
8
+        self.api          = api
9
+        self.audio        = audio
10
+        self.rtAnalyzer   = RT_Analyzer()
11
+        self.holdDutyPctD = holdDutyPctD
12
+        self.durMs        = 500
13
+        self.mode         = "across"
14
+        self.state        = "off"
15
+        self.minPitch     = 21
16
+        self.maxPitch     = 108
17
+        self.velMapD      = {}
18
+
19
+        self.curMaxPitch  = self.maxPitch
20
+        self.curMinPitch  = self.minPitch
21
+        self.curPitch     = 21
22
+        self.curVelocity  = 0
23
+        self.curEndMs     = 0
24
+        self.curBegNoteMs = 0
25
+        self.curEndNoteMs = 0
26
+        
27
+        with open(fn,"r") as f:
28
+            d = json.load(f)
29
+
30
+            for pitch,value in d.items():
31
+                self.velMapD[ int(pitch) ] = [ int(x[0]) for x in d[pitch] ]
32
+
33
+        assert self.minPitch in self.velMapD
34
+        assert self.maxPitch in self.velMapD
35
+
36
+        
37
+    def start( self, minPitch, maxPitch, mode  ):
38
+        self.curMaxPitch = maxPitch
39
+        self.curMinPitch = minPitch
40
+        self.curPitch    = minPitch
41
+        self.curVelocity = 0
42
+        self.state       = "note_on"
43
+        self.mode        = mode 
44
+        self.audio.record_enable(True)   # start recording audio
45
+           
46
+    def stop( self ):
47
+        self.curPitch = self.minPitch
48
+        self._all_notes_off()
49
+        self.audio.record_enable(False)
50
+
51
+    def tick( self, ms ):
52
+        if self.state == "off":
53
+            pass
54
+        
55
+        elif self.state == "note_on":
56
+            self.state = self._note_on(ms)
57
+            
58
+        elif self.state == "playing":
59
+            if ms >= self.curEndMs:
60
+                self.state = "note_off"
61
+        
62
+        elif self.state == "note_off":
63
+            self.state = self._note_off(ms)
64
+
65
+
66
+    def _get_duty_cycle( self, pitch, usec ):
67
+        usDutyL = self.holdDutyPctD[pitch]
68
+
69
+        for i in range(len(usDutyL)):
70
+            if usDutyL[i][0] >= usec:
71
+                return usDutyL[i][1]
72
+
73
+        return usDutyL[-1][1]
74
+
75
+    def _calc_next_pitch( self ):
76
+
77
+        self.curPitch += 1
78
+        while self.curPitch not in self.velMapD and self.curPitch <= self.curMaxPitch:
79
+            self.curPitch+1
80
+
81
+        return self.curPitch <= self.curMaxPitch
82
+    
83
+    def _get_next_note_params( self ):
84
+
85
+        usec    = None
86
+        dutyPct = None
87
+        doneFl  = False
88
+
89
+        if self.mode == "updown":
90
+            if self.curVelocity + 1 < len(self.velMapD[ self.curPitch ]):
91
+                self.curVelocity += 1
92
+            else:
93
+
94
+                if self._calc_next_pitch():
95
+                    self.curVelocity = 0
96
+                else:
97
+                    doneFl = True
98
+
99
+        else:
100
+            
101
+            if self._calc_next_pitch():
102
+                self.curPitch += 1
103
+            else:
104
+                if self.curVelocity + 1 < len(self.velMapD[ self.curPitch ]):
105
+                    self.curVelocity += 1
106
+                    self.curPitch = self.curMinPitch
107
+                else:
108
+                    doneFl = True
109
+
110
+        if doneFl:
111
+            self.audio.record_enable(False)
112
+        else:
113
+            usec    = self.velMapD[self.curPitch][self.curVelocity]
114
+        
115
+            dutyPct = self._get_duty_cycle( self.curPitch, usec )
116
+            
117
+
118
+        return self.curPitch, usec, dutyPct
119
+            
120
+    def _note_on( self, ms ):
121
+
122
+        pitch,usec,dutyPct = self._get_next_note_params()
123
+
124
+        if not usec:
125
+            return "off"
126
+        else:
127
+            print(self.curPitch,self.curVelocity,usec,dutyPct)
128
+            self.curBegNoteMs = self.audio.buffer_sample_ms().value
129
+            self.api.set_pwm_duty( pitch, dutyPct )
130
+            self.api.note_on_us( pitch, usec )
131
+            self.curEndMs = ms + self.durMs
132
+            return "playing"
133
+        
134
+    def _note_off( self, ms ):
135
+
136
+        self.curEndNoteMs = self.audio.buffer_sample_ms().value
137
+        self.rtAnalyzer.analyze_note( self.audio, self.curPitch, self.curBegNoteMs, self.curEndNoteMs, self.cfg.analysisArgs['rmsAnalysisArgs'] )
138
+        self.api.note_off( self.curPitch )
139
+        return "note_on"
140
+
141
+
142
+    def _all_notes_off( self ):
143
+        if self.curPitch == 109:
144
+            self.state = 'off'
145
+            print('done')
146
+        else:
147
+            self.api.note_off( self.curPitch )
148
+            self.curPitch += 1

Loading…
Cancel
Save