circuitpython-keymanager 1.1.1__tar.gz → 1.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/PKG-INFO +1 -1
  2. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/circuitpython_keymanager.egg-info/PKG-INFO +1 -1
  3. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/pyproject.toml +1 -1
  4. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/relic_keymanager.py +124 -10
  5. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md +0 -0
  6. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/.github/workflows/build.yml +0 -0
  7. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/.github/workflows/failure-help-text.yml +0 -0
  8. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/.github/workflows/release_gh.yml +0 -0
  9. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/.github/workflows/release_pypi.yml +0 -0
  10. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/.gitignore +0 -0
  11. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/.pre-commit-config.yaml +0 -0
  12. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/.readthedocs.yaml +0 -0
  13. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/CODE_OF_CONDUCT.md +0 -0
  14. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/LICENSE +0 -0
  15. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/LICENSES/CC-BY-4.0.txt +0 -0
  16. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/LICENSES/MIT.txt +0 -0
  17. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/LICENSES/Unlicense.txt +0 -0
  18. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/README.rst +0 -0
  19. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/README.rst.license +0 -0
  20. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/circuitpython_keymanager.egg-info/SOURCES.txt +0 -0
  21. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/circuitpython_keymanager.egg-info/dependency_links.txt +0 -0
  22. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/circuitpython_keymanager.egg-info/requires.txt +0 -0
  23. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/circuitpython_keymanager.egg-info/top_level.txt +0 -0
  24. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/docs/_static/favicon.ico +0 -0
  25. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/docs/_static/favicon.ico.license +0 -0
  26. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/docs/api.rst +0 -0
  27. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/docs/api.rst.license +0 -0
  28. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/docs/conf.py +0 -0
  29. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/docs/examples.rst +0 -0
  30. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/docs/examples.rst.license +0 -0
  31. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/docs/index.rst +0 -0
  32. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/docs/index.rst.license +0 -0
  33. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/docs/requirements.txt +0 -0
  34. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/examples/keymanager_arpeggiator.py +0 -0
  35. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/examples/keymanager_keys.py +0 -0
  36. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/examples/keymanager_sequencer.py +0 -0
  37. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/examples/keymanager_simpletest.py +0 -0
  38. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/examples/keymanager_synthio.py +0 -0
  39. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/optional_requirements.txt +0 -0
  40. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/requirements.txt +0 -0
  41. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/ruff.toml +0 -0
  42. {circuitpython_keymanager-1.1.1 → circuitpython_keymanager-1.2.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: circuitpython-keymanager
3
- Version: 1.1.1
3
+ Version: 1.2.1
4
4
  Summary: Tools to manage notes in musical applications. Includes note priority, arpeggiation, and sequencing.
5
5
  Author-email: Cooper Dalrymple <me@dcdalrymple.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: circuitpython-keymanager
3
- Version: 1.1.1
3
+ Version: 1.2.1
4
4
  Summary: Tools to manage notes in musical applications. Includes note priority, arpeggiation, and sequencing.
5
5
  Author-email: Cooper Dalrymple <me@dcdalrymple.com>
6
6
  License: MIT
@@ -13,7 +13,7 @@ requires = [
13
13
  [project]
14
14
  name = "circuitpython-keymanager"
15
15
  description = "Tools to manage notes in musical applications. Includes note priority, arpeggiation, and sequencing."
16
- version = "1.1.1"
16
+ version = "1.2.1"
17
17
  readme = "README.rst"
18
18
  authors = [
19
19
  {name = "Cooper Dalrymple", email = "me@dcdalrymple.com"}
@@ -24,7 +24,7 @@ Implementation Notes
24
24
 
25
25
  # imports
26
26
 
27
- __version__ = "1.1.1"
27
+ __version__ = "1.2.1"
28
28
  __repo__ = "https://github.com/relic-se/CircuitPython_KeyManager.git"
29
29
 
30
30
  import asyncio
@@ -331,10 +331,26 @@ class Timer:
331
331
 
332
332
  _last_press: list[int] = []
333
333
 
334
- async def update(self):
334
+ def update(self) -> None:
335
335
  """Update the timer object and call any relevant callbacks if a new beat step or the end of
336
- the gate of a step is reached. The actual functionality of this method will depend on the
337
- child class that utilizes the :class:`Timer` parent class.
336
+ the gate of a step is reached. For best performance, call this method frequently! The
337
+ actual functionality of this method will depend on the child class that utilizes the
338
+ :class:`Timer` parent class.
339
+ """
340
+ if not self._active:
341
+ return
342
+ current = time.monotonic()
343
+ if self._last_press and current - self._now >= self._gate_duration:
344
+ self._do_release()
345
+ if current - self._now >= self._step_time:
346
+ self._update()
347
+ self._do_step()
348
+ self._now += self._step_time
349
+
350
+ async def update_async(self):
351
+ """Update the timer object using asyncio and call any relevant callbacks if a new beat step
352
+ or the end of the gate of a step is reached. The actual functionality of this method will
353
+ depend on the child class that utilizes the :class:`Timer` parent class.
338
354
  """
339
355
  while True:
340
356
  if not self._active:
@@ -534,6 +550,16 @@ class Arpeggiator(Timer):
534
550
  self._do_press(self._notes[self._pos].notenum, self._notes[self._pos].velocity)
535
551
 
536
552
 
553
+ class LoopType:
554
+ """An enum-like class representing sequencer looping methods."""
555
+
556
+ LOOP: int = const(0)
557
+ """When the end of a loop is reached, return back to the start."""
558
+
559
+ SINGLE: int = const(1)
560
+ """When the end of a loop is reached, stop sequence."""
561
+
562
+
537
563
  class Sequencer(Timer):
538
564
  """Sequence notes using the :class:`Timer` class to create a multi-track note sequencer. By
539
565
  default, the Sequencer is set up for a single 4/4 measure of 16 notes with one track. Each note
@@ -551,6 +577,9 @@ class Sequencer(Timer):
551
577
  self.tracks = tracks
552
578
  self._data = [[None for j in range(self._length)] for i in range(self._tracks)]
553
579
 
580
+ def _get_end(self) -> int:
581
+ return min(self._loop_end, self._length) if self._loop_end is not None else self._length
582
+
554
583
  _data: list = None
555
584
 
556
585
  _length: int = 16
@@ -558,9 +587,10 @@ class Sequencer(Timer):
558
587
  @property
559
588
  def length(self) -> int:
560
589
  """The number of steps for each track. If the length is shortened, all of the step data
561
- beyond the new length will be deleted, and if the sequencer is also currently running, it
562
- should loop back around automatically to the start of the track data. The minimum allowed
563
- is 1.
590
+ beyond the new length will be deleted and the :attr:`loop_start` and :attr:`loop_end`
591
+ properties may be altered. If the sequencer is currently running, it should loop back around
592
+ automatically to the start of the track data if :attr:`position` is beyond the new value.
593
+ The minimum length allowed is 1.
564
594
  """
565
595
  return self._length
566
596
 
@@ -575,6 +605,9 @@ class Sequencer(Timer):
575
605
  for i in range(self._tracks):
576
606
  del self._data[i][value:]
577
607
  self._length = value
608
+ if self._loop_end is not None:
609
+ self._loop_end = min(self._loop_end, self._length)
610
+ self._loop_start = min(self._loop_start, self._get_end() - 1)
578
611
 
579
612
  _tracks: int = 1
580
613
 
@@ -598,6 +631,47 @@ class Sequencer(Timer):
598
631
  del self._data[value:]
599
632
  self._tracks = value
600
633
 
634
+ _loop_start: int = 0
635
+
636
+ @property
637
+ def loop_start(self) -> int:
638
+ """The index of the sequence of which to begin at when looping back. This occurs when
639
+ :attr:`position` reaches :attr:`loop_end`. Should be a value between 0 and :attr:`loop_end`
640
+ - 1.
641
+ """
642
+ return self._loop_start
643
+
644
+ @loop_start.setter
645
+ def loop_start(self, value: int) -> int:
646
+ self._loop_start = min(max(value, 0), self._get_end() - 1)
647
+
648
+ _loop_end: int | None = None
649
+
650
+ @property
651
+ def loop_end(self) -> int | None:
652
+ """The index of the sequence of which to loop back from when :attr:`position` reaches it.
653
+ Should be a value between :attr:`loop_start` + 1 and :attr:`length`. If set as `None`, this
654
+ value will be ignored and :attr:`length` will be used instead.
655
+ """
656
+ return self._loop_end
657
+
658
+ @loop_end.setter
659
+ def loop_end(self, value: int | None) -> None:
660
+ self._loop_end = (
661
+ min(max(value, self._loop_start + 1), self._length) if value is not None else value
662
+ )
663
+
664
+ _loop_type: int = LoopType.LOOP
665
+
666
+ @property
667
+ def loop_type(self) -> int:
668
+ """The method of looping through the sequence. See :class:`LoopType` for options."""
669
+ return self._mode
670
+
671
+ @loop_type.setter
672
+ def loop_type(self, value: int) -> None:
673
+ self._loop_type = value % 2
674
+
601
675
  _pos: int = 0
602
676
 
603
677
  @property
@@ -605,6 +679,12 @@ class Sequencer(Timer):
605
679
  """The current position of the sequencer within the track length (0-based)."""
606
680
  return self._pos
607
681
 
682
+ @position.setter
683
+ def position(self, value: int) -> None:
684
+ self._pos = value % self._length
685
+ if self._last_press:
686
+ self._do_release()
687
+
608
688
  def set_note(self, position: int, notenum: int, velocity: float = 1.0, track: int = 0) -> None:
609
689
  """Set the note value and velocity of a track at a specific step index.
610
690
 
@@ -662,8 +742,21 @@ class Sequencer(Timer):
662
742
  called. Must have 1 parameter for sequencer position index. Ie: :code:`def step(pos):`.
663
743
  """
664
744
 
745
+ on_loop: Callable[[int], None] = None
746
+ """The callback method that is called when a the sequencer :attr:`position` reaches
747
+ :attr:`loop_end` and begins back at :attr:`loop_start`. Must have 1 parameter for sequencer
748
+ position index, typically :attr:`loop_start`. Ie: :code:`def loop(pos):`.
749
+ """
750
+
665
751
  def _update(self):
666
- self._pos = (self._pos + 1) % self._length
752
+ self._pos += 1
753
+ end = self._get_end()
754
+ if self._pos >= end:
755
+ self._pos = min(max(self._loop_start, 0), end - 1)
756
+ if self._loop_type == LoopType.SINGLE:
757
+ self.active = False
758
+ return
759
+ self._do_loop()
667
760
  for i in range(self._tracks):
668
761
  note = self._data[i][self._pos]
669
762
  if note and note[0] > 0 and note[1] > 0:
@@ -673,6 +766,10 @@ class Sequencer(Timer):
673
766
  if callable(self.on_step):
674
767
  self.on_step(self._pos)
675
768
 
769
+ def _do_loop(self):
770
+ if callable(self.on_loop):
771
+ self.on_loop(self._pos)
772
+
676
773
 
677
774
  class KeyboardMode:
678
775
  """An enum-like class representing Keyboard note handling modes."""
@@ -843,8 +940,25 @@ class Keyboard:
843
940
  self._sustained = [note for note in self._sustained if note != notenum]
844
941
  self._update()
845
942
 
846
- async def update(self, delay: float = 0.01) -> None:
847
- """Update :attr:`keys` objects if they were provided during initialization.
943
+ def update(self) -> None:
944
+ """Update :attr:`keys` objects if they were provided during initialization. For best
945
+ performance, call frequently!
946
+
947
+ :param delay: The amount of time to sleep between polling in seconds.
948
+ """
949
+ if event := self._keys.events.get():
950
+ notenum = self.root + event.key_number
951
+ if event.pressed:
952
+ self.append(notenum, keynum=event.key_number)
953
+ if callable(self.on_key_press):
954
+ self.on_key_press(event.key_number, notenum, 1.0)
955
+ elif event.released:
956
+ self.remove(notenum)
957
+ if callable(self.on_key_release):
958
+ self.on_key_release(event.key_number, notenum)
959
+
960
+ async def update_async(self, delay: float = 0.01) -> None:
961
+ """Update :attr:`keys` objects if they were provided during initialization using asyncio.
848
962
 
849
963
  :param delay: The amount of time to sleep between polling in seconds.
850
964
  """