CircuitPython_JEplayer_mp3: add

This commit is contained in:
Jeff Epler 2020-05-15 09:54:51 -05:00
parent e6f01ecd5e
commit b5bc36b448
8 changed files with 5586 additions and 0 deletions

View file

@ -0,0 +1,71 @@
# The MIT License (MIT)
#
# Copyright (c) 2020 Jeff Epler for Adafruit Industries LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
Convert an analog joystick to digital
"""
import analogio
import board
class AnalogJoystick:
"""Convert an analog joystick to digital"""
def __init__(self, pin_x=None, pin_y=None, x_invert=False, y_invert=True, deadzone=8000):
self._x = analogio.AnalogIn(pin_x or board.JOYSTICK_X)
self._y = analogio.AnalogIn(pin_y or board.JOYSTICK_Y)
self.x_invert = x_invert
self.y_invert = y_invert
self.deadzone = deadzone
self.recenter()
self.poll()
def poll(self):
"""Read the analog values and update the digital outputs"""
self.x = (self._x.value - self.x_center) * (-1 if self.x_invert else 1)
self.y = (self._y.value - self.y_center) * (-1 if self.y_invert else 1)
return [self.up, self.down, self.left, self.right]
# pylint: disable=invalid-name
@property
def up(self):
"""Return true when the stick was pressed up at the last poll"""
return self.y > self.deadzone
# pylint: enable=invalid-name
@property
def down(self):
"""Return true when the stick was pressed down at the last poll"""
return self.y < -self.deadzone
@property
def left(self):
"""Return true when the stick was pressed left at the last poll"""
return self.x < -self.deadzone
@property
def right(self):
"""Return true when the stick was pressed right at the last poll"""
return self.x > self.deadzone
def recenter(self):
"""Use the current position of the analog joystick as the center"""
self.x_center = self._x.value
self.y_center = self._y.value

View file

@ -0,0 +1,525 @@
# The MIT License (MIT)
#
# Copyright (c) 2020 Jeff Epler for Adafruit Industries LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
jeplayer - main file
This is an MP3 player for the PyGamer with CircuitPython.
See README.md for more information.
"""
import gc
import os
import random
import time
import adafruit_bitmap_font.bitmap_font
import adafruit_display_text.label
from adafruit_progressbar import ProgressBar
import adafruit_sdcard
import analogjoy
import audioio
import audiomp3
import board
import busio
import digitalio
import displayio
import terminalio
import gamepadshift
import icons
import neopixel
import repeat
import storage
from micropython import const
def clear_display():
"""Display nothing"""
board.DISPLAY.show(displayio.Group(max_size=1))
clear_display()
# pylint: disable=invalid-name
def px(x, y):
"""Convert a raw value (x/y) to a pixel value, clamping negative values"""
return 0 if x <= 0 else round(x / y)
# pylint: enable=invalid-name
(ICON_PLAY, ICON_PAUSE, ICON_STOP, ICON_PREV, ICON_NEXT, ICON_REPEAT,
ICON_SHUFFLE, ICON_FOLDERNEXT) = range(8)
class PlaybackDisplay:
"""Manage display during playback"""
def __init__(self):
self.group = displayio.Group(max_size=4)
self.glyph_width, self.glyph_height = font.get_bounding_box()[:2]
self.pbar = ProgressBar(0, 0, board.DISPLAY.width,
self.glyph_height, bar_color=0x0000ff,
outline_color=0x333333, stroke=1)
self.iconbar = icons.IconBar()
self.iconbar.group.y = 112
for i in range(5, 8):
self.iconbar.icons[i].x += 32
self.label = adafruit_display_text.label.Label(font, line_spacing=1.0,
max_glyphs=256)
self.label.y = 6
self._bitmap_filename = None
self._fallback_bitmap = ["/rsrc/background.bmp"]
self.set_bitmap([]) # Must be first!
self.group.append(self.pbar)
self.group.append(self.label)
self.group.append(self.iconbar.group)
self.pixels = neopixel.NeoPixel(board.NEOPIXEL, 5)
self.pixels.auto_write = False
self.pixels.fill(0)
self.pixels.show()
self.paused = False
self.next_choice = 0
@property
def text(self):
"""The text shown at the top of the display. Usually 2 lines."""
return self._text
@text.setter
def text(self, text):
if len(text) > 256:
text = text[:256]
self._text = text
self.label.text = text
@property
def progress(self):
"""The fraction of progress through the current track"""
return self.pbar.progress
@progress.setter
def progress(self, frac):
self.pbar.progress = frac
def set_bitmap(self, candidates):
"""Find and use a background from among candidates, or else the fallback bitmap"""
for i in candidates + self._fallback_bitmap:
if i == self._bitmap_filename:
return # Already loaded
try:
bitmap_file = open(i, 'rb')
except OSError:
continue
bitmap = displayio.OnDiskBitmap(bitmap_file)
self._bitmap_filename = i
# Create a TileGrid to hold the bitmap
self.tile_grid = displayio.TileGrid(bitmap, pixel_shader=displayio.ColorConverter())
# Add the TileGrid to the Group
if len(self.group) == 0:
self.group.append(self.tile_grid)
else:
self.group[0] = self.tile_grid
self.tile_grid.x = (160 - bitmap.width) // 2
self.tile_grid.y = self.glyph_height*2 + max(0, (96 - bitmap.height) // 2)
break
@property
def rms(self):
"""The RMS audio level, used to control the neopixel vu meter"""
return self._rms
@rms.setter
def rms(self, value):
self._rms = value
self.pixels[0] = (20, 0, 0) if value > 20 else (px(value, 1), 0, 0)
self.pixels[1] = (20, 0, 0) if value > 40 else (px(value - 20, 1), 0, 0)
self.pixels[2] = (20, 0, 0) if value > 80 else (px(value - 40, 2), 0, 0)
self.pixels[3] = (20, 0, 0) if value > 160 else (px(value - 80, 4), 0, 0)
self.pixels[4] = (20, 0, 0) if value > 320 else (px(value - 160, 8), 0, 0)
self.pixels.show()
# pylint: disable=too-many-branches
def press(self, idx):
"""Do the action for the current icon"""
selected = self.iconbar.selected
if selected in (ICON_PLAY, ICON_PAUSE): # Play/Pause
if self.paused:
self.resume()
else:
self.pause()
self.iconbar.select(not self.paused)
elif selected == ICON_STOP:
self.iconbar.deactivate(ICON_FOLDERNEXT)
return (-1,)
elif selected == ICON_PREV:
if self.shuffle:
return (None,)
return (idx-1,)
elif selected == ICON_NEXT:
if self.shuffle:
return (None,)
return (idx+1,)
elif selected == ICON_SHUFFLE:
self.iconbar.toggle(selected)
if self.iconbar.active[ICON_SHUFFLE]:
self.iconbar.deactivate(ICON_REPEAT)
self.iconbar.deactivate(ICON_FOLDERNEXT)
elif selected == ICON_REPEAT:
self.iconbar.toggle(selected)
if self.iconbar.active[ICON_REPEAT]:
self.iconbar.deactivate(ICON_SHUFFLE)
self.iconbar.deactivate(ICON_FOLDERNEXT)
elif selected == ICON_FOLDERNEXT:
self.iconbar.toggle(selected)
if self.iconbar.active[ICON_FOLDERNEXT]:
self.iconbar.deactivate(ICON_REPEAT)
self.iconbar.deactivate(ICON_SHUFFLE)
return None
def move(self, direction):
"""Switch the current icon in the given direction"""
self.iconbar.select((self.iconbar.selected + direction) % 8)
def play(self, stream):
"""Starting playing a stream on the speaker"""
speaker.play(stream)
self.paused = False
self.iconbar.set_active(0, not self.paused)
self.iconbar.set_active(1, self.paused)
def pause(self):
"""Pause the stream"""
speaker.pause()
self.paused = True
self.iconbar.set_active(0, not self.paused)
self.iconbar.set_active(1, self.paused)
def resume(self):
"""Resume the stream"""
speaker.resume()
self.paused = False
self.iconbar.set_active(0, not self.paused)
self.iconbar.set_active(1, self.paused)
@property
def shuffle(self):
"""Whether to shuffle the playlist"""
return self.iconbar.active[ICON_SHUFFLE]
@property
def repeat(self):
"""Whether to repeat the playlist"""
return self.iconbar.active[ICON_REPEAT]
@property
def auto_next(self):
"""Whether to play all folders"""
return self.iconbar.active[ICON_FOLDERNEXT]
@staticmethod
def has_any_mp3s(folder):
"""True if the folder contains at least one item ending in .mp3"""
return any(fn.lower().endswith(".mp3") for fn in os.listdir(folder))
def choose_folder(self, base='/sd'):
"""Let the user choose a folder within a base directory"""
all_folders = (m for m in os.listdir(base)
if not m.startswith('.') and isdir(join(base, m)))
all_folders = sorted(f for f in all_folders if self.has_any_mp3s(join(base, f)))
choices = ['Surprise Me'] + all_folders
if playback_display.auto_next:
idx = self.next_choice
else:
idx = menu_choice(choices,
BUTTON_START | BUTTON_A | BUTTON_B | BUTTON_SEL,
sel_idx=self.next_choice,
text_font=terminalio.FONT)
clear_display()
self.next_choice = idx
if idx >= 1:
result = all_folders[idx-1]
self.next_choice = idx+1
if self.next_choice == len(choices):
self.next_choice = 1 # Go to first folder, not "surprise me"
else:
result = random.choice(all_folders)
return join(base, result)
# pylint: disable=invalid-name
enable = digitalio.DigitalInOut(board.SPEAKER_ENABLE)
enable.direction = digitalio.Direction.OUTPUT
enable.value = True
speaker = audioio.AudioOut(board.SPEAKER, right_channel=board.A1)
mp3stream = audiomp3.MP3Decoder(open("/rsrc/splash.mp3", "rb"))
speaker.play(mp3stream)
font = adafruit_bitmap_font.bitmap_font.load_font("rsrc/5x8.bdf")
playback_display = PlaybackDisplay()
board.DISPLAY.show(playback_display.group)
font.load_glyphs(range(32, 128))
BUTTON_SEL = const(8)
BUTTON_START = const(4)
BUTTON_A = const(2)
BUTTON_B = const(1)
joystick = analogjoy.AnalogJoystick()
up_key = repeat.KeyRepeat(lambda: joystick.up, rate=0.2)
down_key = repeat.KeyRepeat(lambda: joystick.down, rate=0.2)
left_key = repeat.KeyRepeat(lambda: joystick.left, rate=0.2)
right_key = repeat.KeyRepeat(lambda: joystick.right, rate=0.2)
buttons = gamepadshift.GamePadShift(digitalio.DigitalInOut(board.BUTTON_CLOCK),
digitalio.DigitalInOut(board.BUTTON_OUT),
digitalio.DigitalInOut(board.BUTTON_LATCH))
# pylint: enable=invalid-name
def mount_sd():
"""Mount the SD card"""
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
sd_cs = digitalio.DigitalInOut(board.SD_CS)
sdcard = adafruit_sdcard.SDCard(spi, sd_cs, baudrate=6000000)
vfs = storage.VfsFat(sdcard)
storage.mount(vfs, "/sd")
def join(*args):
"""Like posixpath.join"""
return "/".join(args)
def shuffle(seq):
"""Shuffle a sequence using the Fisher-Yates shuffle algorithm (like random.shuffle)"""
for i in range(len(seq)-2):
j = random.randint(i, len(seq)-1)
seq[i], seq[j] = seq[j], seq[i]
# pylint: disable=too-many-locals,too-many-statements
def menu_choice(seq, button_ok, button_cancel=0, *, sel_idx=0, text_font=font):
"""Display a menu and allow a choice from it"""
gc.collect()
board.DISPLAY.auto_refresh = True
scroll_idx = 0
glyph_width, glyph_height = text_font.get_bounding_box()[:2]
num_rows = min(len(seq), board.DISPLAY.height // glyph_height)
max_glyphs = board.DISPLAY.width // glyph_width
palette = displayio.Palette(2)
palette[0] = 0
palette[1] = 0xffffff
labels = [displayio.TileGrid(text_font.bitmap, pixel_shader=palette,
width=max_glyphs+1, height=1,
tile_width=glyph_width,
tile_height=glyph_height)
for i in range(num_rows)]
terminals = [terminalio.Terminal(li, text_font) for li in labels]
cursor = adafruit_display_text.label.Label(text_font, max_glyphs=1, color=0xddddff)
base_y = 0
caret_offset = glyph_height//2-1
scene = displayio.Group(max_size=len(labels) + 1)
for i, label in enumerate(labels):
label.x = round(glyph_width * 1.5)
label.y = base_y + glyph_height * i
scene.append(label)
cursor.x = 0
cursor.y = caret_offset
cursor.text = ">"
scene.append(cursor)
last_scroll_idx = max(0, len(seq) - num_rows)
board.DISPLAY.show(scene)
buttons.get_pressed() # Clear out anything from before now
i = 0
old_scroll_idx = None
while True:
enable.value = speaker.playing
pressed = buttons.get_pressed()
if button_cancel and (pressed & button_cancel):
return -1
if pressed & button_ok:
return sel_idx
joystick.poll()
if up_key.value:
sel_idx -= 1
if down_key.value:
sel_idx += 1
sel_idx = min(len(seq)-1, max(0, sel_idx))
if scroll_idx > sel_idx or scroll_idx + num_rows <= sel_idx:
scroll_idx = sel_idx - num_rows // 2
scroll_idx = min(last_scroll_idx, max(0, scroll_idx))
board.DISPLAY.auto_refresh = False
if old_scroll_idx != scroll_idx:
for i in range(scroll_idx, scroll_idx + num_rows):
j = i - scroll_idx
new_text = ''
if i < len(seq):
new_text = seq[i][:max_glyphs]
terminals[j].write('\r\033[K')
terminals[j].write(new_text)
cursor.y = caret_offset + base_y + glyph_height * (sel_idx - scroll_idx)
board.DISPLAY.auto_refresh = True
old_scroll_idx = scroll_idx
time.sleep(1/20)
# pylint: enable=too-many-locals
S_IFDIR = const(16384)
def isdir(x):
"""Return True if 'x' is a directory"""
return os.stat(x)[0] & S_IFDIR
def wait_no_button_pressed():
"""Wait until no button is pressed"""
while buttons.get_pressed():
time.sleep(1/20)
def change_stream(filename):
"""Change the global MP3Decoder object to play a new file"""
old_stream = mp3stream.file
mp3stream.file = open(filename, "rb")
old_stream.close()
return mp3stream.file
def play_one_file(idx, filename, folder, title, playlist_size):
"""Play one file, reacting to user input"""
board.DISPLAY.auto_refresh = False
playback_display.set_bitmap([
filename.rsplit('.', 1)[0] + ".bmp",
filename.rsplit('/', 1)[0] + ".bmp",
filename.rsplit('/', 1)[0] + "/cover.bmp",
])
playback_display.text = "%s\n%s" % (folder, title)
board.DISPLAY.refresh()
result = None
wait_no_button_pressed()
file_size = os.stat(filename)[6]
mp3file = change_stream(filename)
playback_display.play(mp3stream)
board.DISPLAY.auto_refresh = True
last_pressed = buttons.get_pressed()
while speaker.playing:
# pylint: disable=no-member
if gc.mem_free() < 4096:
gc.collect()
playback_display.rms = mp3stream.rms_level
playback_display.progress = mp3file.tell() / file_size
joystick.poll()
if left_key.value:
playback_display.move(-1)
if right_key.value:
playback_display.move(1)
pressed = buttons.get_pressed()
rising_edge = pressed & ~last_pressed
last_pressed = pressed
if rising_edge:
return_now = playback_display.press(idx)
wait_no_button_pressed()
if return_now:
result = return_now[0]
break
if result is None:
if playback_display.shuffle:
if playback_display.shuffle:
# Choose a random integer .. except for this one
result = random.randrange(playlist_size-1)
if result >= idx:
result += 1
else:
result = (idx + 1)
speaker.stop()
playback_display.rms = 0
gc.collect()
return result
def play_all(playlist, *, folder='', trim=0, location='/sd'):
"""Play everything in 'playlist', which is relative to 'location'.
'folder' is a display name for the user."""
i = 0
board.DISPLAY.show(playback_display.group)
while 0 <= i < len(playlist):
filename = playlist[i]
i = play_one_file(i, join(location, filename), folder, filename[trim:-4], len(playlist))
if i == -1:
break
if playback_display.repeat and i == len(playlist):
i = 0
speaker.stop()
clear_display()
def longest_common_prefix(seq):
"""Find the longest common prefix between all items in sequence"""
seq0 = seq[0]
for i, seq0i in enumerate(seq0):
for j in seq:
if len(j) < i or j[i] != seq0i:
return i
return len(seq0)
def play_folder(location):
"""Play everything within a given folder"""
playlist = [d for d in os.listdir(location) if d.lower().endswith('.mp3')]
if not playlist:
# hmm, no mp3s in a folder? Well, don't crash okay?
del playlist
gc.collect()
return
playlist.sort()
trim = longest_common_prefix(playlist)
enable.value = True
play_all(playlist, folder=location.split('/')[-1], trim=trim, location=location)
enable.value = False
def main():
"""The main function of the player"""
try:
mount_sd()
except OSError as detail:
text = "%s\n\nInsert or re-seat\nSD card\nthen press reset" % detail.args[0]
error_text = adafruit_display_text.label.Label(font, text=text)
error_text.x = 8
error_text.y = board.DISPLAY.height // 2
g = displayio.Group()
g.append(error_text)
board.DISPLAY.show(g)
while True:
time.sleep(1)
while True:
folder = playback_display.choose_folder()
play_folder(folder)
main()

View file

@ -0,0 +1,111 @@
# The MIT License (MIT)
#
# Copyright (c) 2020 Jeff Epler for Adafruit Industries LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
Icon Bar suitable for navigation by joystick
"""
import adafruit_imageload.bmp
import displayio
def make_palette(seq):
"""Create a palette from a sequence of colors"""
pal = displayio.Palette(len(seq))
for i, color in enumerate(seq):
if color is None:
pal.make_transparent(i)
else:
pal[i] = color
return pal
BLACK, WHITE, RED, BLUE = 0x111111, 0xffffff, 0xff0000, 0x0000ff
PALETTE_NORMAL = make_palette([BLACK, WHITE, BLACK, BLACK])
PALETTE_SELECTED = make_palette([BLACK, WHITE, RED, BLACK])
PALETTE_ACTIVE = make_palette([BLACK, WHITE, BLACK, BLUE])
PALETTE_BOTH = make_palette([BLACK, WHITE, RED, BLUE])
PALETTES = [PALETTE_NORMAL, PALETTE_ACTIVE, PALETTE_SELECTED, PALETTE_BOTH]
class IconBar:
"""An icon bar presents n 16x16 icons in a row.
One icon can be "selected" and any number can be "active"."""
def __init__(self, n=8, filename="/rsrc/icons.bmp"):
self.group = displayio.Group(max_size=n)
self.bitmap_file = open(filename, "rb")
self.bitmap = adafruit_imageload.bmp.load(self.bitmap_file, bitmap=displayio.Bitmap)[0]
self._selected = None
self.icons = [displayio.TileGrid(self.bitmap,
pixel_shader=PALETTE_NORMAL, x=16*i,
y=0, width=1, height=1,
tile_width=16, tile_height=16)
for i in range(n)]
self.active = [False] * n
for i, icon in enumerate(self.icons):
icon[0] = i
self.group.append(icon)
self.select(0)
@property
def selected(self):
"""The currently selected icon"""
return self._selected
@selected.setter
def selected(self, n):
self.select(n)
def select(self, n):
"""Select the n'th icon"""
old_selected = self._selected
self._selected = n
if n != old_selected:
self._refresh(n)
if old_selected is not None:
self._refresh(old_selected)
def set_active(self, n, new_state):
"""Sets the n'th icon's active state to new_state"""
new_state = bool(new_state)
if self.active[n] == new_state:
return
self.active[n] = new_state
self._refresh(n)
def activate(self, n):
"""Set the n'th icon to be active"""
self.set_active(n, True)
def deactivate(self, n):
"""Set the n'th icon to be inactive"""
self.set_active(n, False)
def toggle(self, n):
"""Toggle the state of the n'th icon"""
self.set_active(n, not self.active[n])
print()
def _refresh(self, n):
"""Update the appearance of the n'th icon"""
palette_index = self.active[n] + 2 * (self._selected == n)
self.icons[n].pixel_shader = PALETTES[palette_index]

View file

@ -0,0 +1,47 @@
# The MIT License (MIT)
#
# Copyright (c) 2020 Jeff Epler for Adafruit Industries LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
Make a key (button) repeat when held down
"""
import time
class KeyRepeat:
"""Track the state of a button and, while it is held, output a press every
'rate' seconds"""
def __init__(self, getter, rate=0.5):
self.getter = getter
self.rate_ns = round(rate * 1e9)
self.next = -1
@property
def value(self):
"""True when a button is first pressed, or once every 'rate' seconds
thereafter"""
state = self.getter()
if not state:
self.next = -1
return False
now = time.monotonic_ns()
if state and now > self.next:
self.next = now + self.rate_ns
return True
return False

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.