Skip to content

Commit 5be93b0

Browse files
Merge pull request #146 from wehr-lab/jackd_triggers
Jackd triggers Improvements to sound timing and stability!!! The major change here is in the sound modules - Changed the way that continuous sounds work. Rather than cycling through an array, which was easy to drop, now pass a sound object that can generate its own samples on the fly using the `hydration` module. - More accurate timing of sound ending callbacks. Before, the event would be called immediately on buffering the sounds into the jack ports, but that was systematically too early. Instead, use jack timing methods to account for delay from blocksize and n_periods to wait_until a certain delay to `set()` the event. See `_wait_for_end` # New - `hydration` module for creating and storing autopilot objects between processes and computers! - `@Introspect` made and added to sound classes. Will be moved to root class. Allows storing the parameters given on instantiation. - minor - added exceptions module # Improvements - Made `ALSA_NPERIODS` its own pref - More debugging flags! - Changed `repeat` to false by default in Nodes because it is largely unnecessary in normal use and causes a lot of overhead! - Made a method to debug timing within the sound server, pass `debug_timing=True` to `jackclient` - `Noise` now can generate noise continuously as a model for refactoring sound classes to do that as the main way of doing so in the future. - Incremental improvements to `requires` classes. # Bugfixes - Fixed several lists that would grow indefinitely and cause hard to diagnose memory issues, particularly with continuously operating things like the sound server or streaming data. Using a deque - `__del__` methods cause more problems than they solve, need to start moving towards registering signal handles explicitly rather than trying to clean up as a last resort when removing from memory. Removed from hardware and networking classes or at least wrapped in try blocks for now. - use `str(Path)` rather than passing a `Path` instance to `pkgutil.iter_modules` - use `importlib-metadata` for python3.7 # Deprecations - Removed `jack_apt` because it only causes problems. `jackd_source` works every time and doesn't take that long. -
2 parents d21c01e + f686fd9 commit 5be93b0

30 files changed

+615
-141
lines changed

autopilot/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
__version__ = '0.4.3'
33

44
from autopilot.setup import setup_autopilot
5-
from autopilot.utils.registry import get, get_task, get_hardware, get_names
5+
from autopilot.utils.registry import get, get_task, get_hardware, get_names
6+
from autopilot.utils.hydration import dehydrate, hydrate

autopilot/core/pilot.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,7 @@ def run_task(self, task_class, task_params):
791791
self.task.end()
792792
except Exception as e:
793793
self.logger.exception(f'got exception while stopping task: {e}')
794+
del self.task
794795
self.task = None
795796
row.append()
796797
table.flush()

autopilot/exceptions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""
2+
Custom warnings and exceptions for better testing and diagnosis!
3+
"""
4+
5+
import warnings
6+
7+
class DefaultPrefWarning(UserWarning):
8+
"""
9+
Warn that a default pref value is being accessed
10+
"""
11+

autopilot/external/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ def start_jackd():
110110
if prefs.get('FS'):
111111
jackd_string = jackd_string.replace('-rfs', f"-r{prefs.get('FS')}")
112112

113+
# replace string nperiods with number
114+
if prefs.get('ALSA_NPERIODS'):
115+
jackd_string = jackd_string.replace('-nper', f"-n{prefs.get('ALSA_NPERIODS')}")
116+
113117
# construct rest of launch string!
114118
# if JACKD_MODULE:
115119
# jackd_path = os.path.join(autopilot_jack.__path__._path[0])

autopilot/hardware/__init__.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,6 @@ class Hardware(object):
8282
8383
Primarily for the purpose of defining necessary attributes.
8484
85-
Also defines `__del__` to call `release()` so objects are always released
86-
even if not explicitly.
87-
8885
Attributes:
8986
name (str): unique name used to identify this object within its group.
9087
group (str): hardware group, corresponds to key in prefs.json ``"HARDWARE": {"GROUP": {"ID": {**params}}}``
@@ -250,7 +247,3 @@ def calibration(self, calibration):
250247
self.logger.info(f'Calibration saved to {cal_fn}: \n{calibration}')
251248

252249
self._calibration = calibration
253-
254-
def __del__(self):
255-
self.release()
256-

autopilot/hardware/gpio.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import itertools
2525
import typing
2626
import warnings
27-
27+
from collections import deque as dq
2828

2929
from autopilot import prefs
3030
from autopilot.hardware import Hardware, BOARD_TO_BCM
@@ -310,6 +310,7 @@ def release(self):
310310
Note:
311311
the Hardware metaclass will call this method on object deletion.
312312
"""
313+
self.logger.debug('releasing')
313314
try:
314315
self.pull = None
315316
except:
@@ -697,6 +698,7 @@ def release(self):
697698
"""
698699
Stops and deletes all scripts, sets to :attr:`~.Digital_Out.off`, and calls :meth:`.GPIO.release`
699700
"""
701+
self.logger.debug('releasing')
700702
try:
701703

702704
self.delete_all_scripts()
@@ -723,6 +725,7 @@ class Digital_In(GPIO):
723725
set this event whenever the callback is triggered. Can be used to handle
724726
stage transition logic here instead of the :class:`.Task` object, as is typical.
725727
record (bool): Whether all logic transitions should be recorded as a list of ('EVENT', 'Timestamp') tuples.
728+
max_events (int): Maximum size of the :attr:`.events` deque
726729
**kwargs: passed to :class:`GPIO`
727730
728731
Sets the internal pullup/down resistor to :attr:`.Digital_In.off` and
@@ -733,19 +736,18 @@ class Digital_In(GPIO):
733736
pull and trigger are set by polarity on initialization in digital inputs, unlike other GPIO classes.
734737
They are not mutually synchronized however, ie. after initialization if any one of these attributes are changed, the other two will remain the same.
735738
736-
737739
Attributes:
738740
pig (:meth:`pigpio.pi`): The pigpio connection.
739741
pin (int): Broadcom-numbered pin, converted from the argument given on instantiation
740742
callbacks (list): A list of :meth:`pigpio.callback`s kept to clear them on exit
741743
polarity (int): Logic direction, if 1: off=0, on=1, pull=low, trigger=high and vice versa for 0
742-
events (list): if :attr:`.record` is True, a list of ('EVENT', 'TIMESTAMP') tuples
744+
events (list): if :attr:`.record` is True, a deque of ('EVENT', 'TIMESTAMP') tuples of length ``max_events``
743745
"""
744746
is_trigger=True
745747
type = 'DIGI_IN'
746748
input = True
747749

748-
def __init__(self, pin, event=None, record=True, **kwargs):
750+
def __init__(self, pin, event=None, record=True, max_events=256, **kwargs):
749751
"""
750752
751753
"""
@@ -763,8 +765,7 @@ def __init__(self, pin, event=None, record=True, **kwargs):
763765
self.callbacks = []
764766

765767
# List to store logic transition events
766-
# FIXME: Should be a deque
767-
self.events = []
768+
self.events = dq(maxlen=max_events)
768769

769770
self.record = record
770771
if self.record:
@@ -853,6 +854,7 @@ def release(self):
853854
"""
854855
Clears any callbacks and calls :meth:`GPIO.release`
855856
"""
857+
self.logger.debug('releasing')
856858
self.clear_cb()
857859
super(Digital_In, self).release()
858860

@@ -983,6 +985,7 @@ def release(self):
983985
984986
"""
985987
# FIXME: reimplementing parent release method here because of inconsistent use of self.off -- unify API and fix!!
988+
self.logger.debug('releasing')
986989
try:
987990
self.delete_all_scripts()
988991
self.set(0) # clean values should handle inversion, don't use self.off
@@ -1257,6 +1260,7 @@ def release(self):
12571260
"""
12581261
Release each channel and stop pig without calling superclass.
12591262
"""
1263+
self.logger.debug('releasing')
12601264
for chan in self.channels.values():
12611265
chan.release()
12621266
self.pig.stop()

autopilot/networking/node.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ def send(self, to: Optional[Union[str, list]] = None,
234234
key:str=None,
235235
value:typing.Any=None,
236236
msg:Optional['Message']=None,
237-
repeat:bool=True, flags = None, force_to:bool = False):
237+
repeat:bool=False, flags = None, force_to:bool = False):
238238
"""
239239
Send a message via our :attr:`~.Net_Node.sock` , DEALER socket.
240240

autopilot/networking/station.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def __init__(self,
126126
self.ip = ""
127127

128128

129-
self.file_block = threading.Event() # to wait for file transfer
129+
self.file_block = multiprocessing.Event() # to wait for file transfer
130130

131131
# number messages as we send them
132132
self.msg_counter = count()
@@ -146,7 +146,10 @@ def __init__(self,
146146

147147

148148
def __del__(self):
149-
self.release()
149+
try:
150+
self.release()
151+
except:
152+
pass
150153

151154
def run(self):
152155
"""
@@ -289,7 +292,6 @@ def send(self, to=None, key=None, value=None, msg=None, repeat=True, flags=None)
289292
msg_enc = msg.serialize()
290293

291294
if not msg_enc:
292-
#set_trace(term_size=(80,40))
293295
self.logger.exception('Message could not be encoded:\n{}'.format(str(msg)))
294296
return
295297

autopilot/prefs.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282

8383
#from autopilot.core.loggers import init_logger
8484
from collections import OrderedDict as odict
85+
from autopilot.exceptions import DefaultPrefWarning
8586

8687
class Scopes(Enum):
8788
"""
@@ -395,10 +396,17 @@ class Scopes(Enum):
395396
'depends': 'AUDIOSERVER',
396397
"scope": Scopes.AUDIO
397398
},
399+
'ALSA_NPERIODS': {
400+
'type': 'int',
401+
'text': 'number of buffer periods to use with ALSA sound driver',
402+
'default': 3,
403+
'depends': 'AUDIOSERVER',
404+
'scope': Scopes.AUDIO
405+
},
398406
'JACKDSTRING': {
399407
'type': 'str',
400408
'text': 'Arguments to pass to jackd, see the jackd manpage',
401-
'default': 'jackd -P75 -p16 -t2000 -dalsa -dhw:sndrpihifiberry -P -rfs -n3 -s &',
409+
'default': 'jackd -P75 -p16 -t2000 -dalsa -dhw:sndrpihifiberry -P -rfs -nper -s &',
402410
'depends': 'AUDIOSERVER',
403411
"scope": Scopes.AUDIO
404412
},
@@ -475,7 +483,7 @@ def get(key: typing.Union[str, None] = None):
475483
default_val = globals()['_DEFAULTS'][key]['default']
476484
if key not in globals()['_WARNED']:
477485
globals()['_WARNED'].append(key)
478-
warnings.warn(f'Returning default prefs value {key} : {default_val} (ideally this shouldnt happen and everything should be specified in prefs', UserWarning)
486+
warnings.warn(f'Returning default prefs value {key} : {default_val} (ideally this shouldnt happen and everything should be specified in prefs', DefaultPrefWarning)
479487
return default_val
480488

481489
# if you still can't find a value, None is an unambiguous signal for pref not set
@@ -594,15 +602,24 @@ def init(fn=None):
594602
cal_raw = os.path.join(prefs['BASEDIR'], 'port_calibration.json')
595603

596604
if os.path.exists(cal_path):
597-
with open(cal_path, 'r') as calf:
598-
cal_fns = json.load(calf)
599-
prefs['PORT_CALIBRATION'] = cal_fns
605+
try:
606+
with open(cal_path, 'r') as calf:
607+
cal_fns = json.load(calf)
608+
prefs['PORT_CALIBRATION'] = cal_fns
609+
except json.decoder.JSONDecodeError:
610+
warnings.warn(f'calibration file was malformed. Renaming to avoid using in the future')
611+
os.rename(cal_path, cal_path + '.bak')
600612
elif os.path.exists(cal_raw):
601613
# aka raw calibration results exist but no fit has been computed
602-
luts = compute_calibration(path=cal_raw, do_return=True)
603-
with open(cal_path, 'w') as calf:
604-
json.dump(luts, calf)
605-
prefs['PORT_CALIBRATION'] = luts
614+
try:
615+
luts = compute_calibration(path=cal_raw, do_return=True)
616+
with open(cal_path, 'w') as calf:
617+
json.dump(luts, calf)
618+
prefs['PORT_CALIBRATION'] = luts
619+
except json.decoder.JSONDecodeError:
620+
warnings.warn(f'processed calibration file was malformed. Renaming to avoid using in the future')
621+
os.rename(cal_raw, cal_raw + '.bak')
622+
606623

607624
###########################
608625

autopilot/setup/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from autopilot.setup.scripts import SCRIPTS

0 commit comments

Comments
 (0)