piccal/PolyNoteTester.py

149 lines
4.7 KiB
Python

import types
import time
from random import randrange
class PolyNoteTester:
def __init__( self, cfg, api ):
self.api = api
r = types.SimpleNamespace(**cfg.PolyNoteTester)
self.cfg = r
if r.mode == "simple":
print("mode:simple")
self.schedL = self._gen_simple_sched(r)
else:
print("mode:poly")
self.schedL = self._gen_sched(r)
self.schedL = sorted( self.schedL, key=lambda x: x[0] )
self.nextMs = 0 # next transition time
self.schedIdx = 0 # next event to play
self.isStartedFl = False
#self._report()
def _report( self ):
for t,cmd,pitch,atkUs in self.schedL:
print("%s %6i %3i %5i" % (cmd,t,pitch,atkUs))
def _gen_simple_sched( self, r ):
""" Play each note sequentially from lowest to highest. """
durMs = int(r.minNoteDurMs + (r.maxNoteDurMs - r.minNoteDurMs)/2)
ioMs = int(r.minInterOnsetMs + (r.maxInterOnsetMs - r.minInterOnsetMs)/2)
atkUs = int(r.minAttackUsec + (r.maxAttackUsec - r.minAttackUsec)/2)
schedL = []
t0 = 0
for pitch in range(r.minPitch,r.maxPitch+1):
schedL.append((t0,'on',pitch,atkUs))
schedL.append((t0+durMs,'off',pitch,0))
t0 += durMs + ioMs
return schedL
def _does_note_overlap( self, beg0Ms, end0Ms, beg1Ms, end1Ms ):
""" if note 0 is entirely before or after note 1 """
return not (beg0Ms > end1Ms or end0Ms < beg1Ms)
def _do_any_notes_overlap( self, begMs, endMs, begEndL ):
for beg1,end1 in begEndL:
if self._does_note_overlap(begMs,endMs,beg1,end1):
return True
return False
def _get_last_end_time( self, begEndL ):
end0 = 0
for beg,end in begEndL:
if end > end0:
end0 = end
return end0
def _gen_sched( self, r ):
pitchL = [ randrange(r.minPitch,r.maxPitch) for _ in range(r.noteCount) ]
durMsL = [ randrange(r.minNoteDurMs, r.maxNoteDurMs) for _ in range(r.noteCount) ]
ioMsL = [ randrange(r.minInterOnsetMs, r.maxInterOnsetMs) for _ in range(r.noteCount) ]
atkUsL = [ randrange(r.minAttackUsec, r.maxAttackUsec) for _ in range(r.noteCount) ]
schedL = []
pitchD = {} # pitch: [ (begMs,endMs) ]
t0 = 0
# for each pitch,dur,ioi,atkUs tuple
for pitch,durMs,interOnsetMs,atkUs in zip(pitchL,durMsL,ioMsL,atkUsL):
# calc note begin and end time
begMs = t0
endMs = t0 + durMs
# if this pitch hasn't yet been added to pitchD
if pitch not in pitchD:
pitchD[ pitch ] = [ (begMs,endMs) ]
else:
# if the proposed note overlaps with other notes for this pitch
if self._do_any_notes_overlap( begMs, endMs, pitchD[pitch] ):
# move this pitch past the last note
begMs = self._get_last_end_time( pitchD[pitch] ) + interOnsetMs
endMs = begMs + durMs
# add the new note to pitchD
pitchD[ pitch ].append( (begMs,endMs) )
# update the schedule
schedL.append( (begMs, 'on', pitch, atkUs))
schedL.append( (endMs, 'off', pitch, 0 ))
t0 += interOnsetMs
return schedL
def start( self ):
self.schedIdx = 0
self.nextMs = 0
self.api.set_hold_duty_all(self.cfg.holdDutyPct)
self.isStartedFl = True
def stop( self ):
self.isStartedFl = False
self.api.all_notes_off()
def tick( self, ms ):
while self.isStartedFl and ms >= self.nextMs and self.schedIdx < len(self.schedL):
t0,cmd,pitch,usec = self.schedL[self.schedIdx]
if cmd == 'on':
if pitch not in self.cfg.skipPitchL:
decay_level = self.api.calc_decay_level( usec )
self.api.note_on_us( pitch, usec, decay_level )
print("on %i %i %i" % (pitch,usec,decay_level))
elif cmd == 'off':
if pitch not in self.cfg.skipPitchL:
self.api.note_off( pitch )
print("off %i" % pitch)
self.schedIdx += 1
if self.schedIdx < len(self.schedL):
self.nextMs = ms + (self.schedL[self.schedIdx][0] - t0)
else:
self.isStartedFl = False
print("Done.")