Skip to content

Commit

Permalink
yping everywhere
Browse files Browse the repository at this point in the history
  • Loading branch information
vxgmichel committed Sep 19, 2022
1 parent c41237b commit 74f7380
Show file tree
Hide file tree
Showing 16 changed files with 289 additions and 172 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[flake8]
max-line-length = 88
extend-ignore = E203
extend-ignore = E501, E203
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v2

- name: Build wheels
uses: pypa/cibuildwheel@v2.9.0
uses: pypa/cibuildwheel@v2.10.1

- uses: actions/upload-artifact@v2
with:
Expand Down
21 changes: 17 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
repos:
- repo: https://github.com/ambv/black
rev: 22.6.0
rev: 22.8.0
hooks:
- id: black
language_version: python3
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
rev: 5.0.4
hooks:
- id: flake8
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.2.3 # Use the ref you want to point at
rev: v2.2.3
hooks:
- id: mixed-line-ending
- id: trailing-whitespace
- id: trailing-whitespace
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v0.971"
hooks:
- id: mypy
additional_dependencies: [
"numpy>=1.20",
"asyncssh>=2.9",
"prompt_toolkit>=3.0.29",
"sounddevice",
"samplerate",
"python-xlib; sys_platform == 'linux'",
"pynput; sys_platform != 'linux'",
]
39 changes: 30 additions & 9 deletions gambaterm/audio.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
from typing import Iterator, Optional, TYPE_CHECKING
from queue import Queue, Empty, Full
from contextlib import contextmanager

import numpy as np
import numpy.typing as npt

from .console import Console

# Late import of samplerate
if TYPE_CHECKING:
import samplerate # type: ignore


class AudioOut:

output_rate = 48000
buffer_size = output_rate // 60
output_rate: float = 48000.0
buffer_size: int = int(output_rate // 60)

def __init__(self, input_rate, resampler, speed=1.0):
input_rate: float
speed: float
resampler: "samplerate.Resampler"
queue: Queue[npt.NDArray[np.int16]]
buffer: npt.NDArray[np.int16]
offset: int

def __init__(
self, input_rate: float, resampler: "samplerate.Resampler", speed: float = 1.0
):
self.input_rate = input_rate
self.speed = speed
self.resampler = resampler
Expand All @@ -18,12 +35,12 @@ def __init__(self, input_rate, resampler, speed=1.0):
self.offset = 0

@property
def ratio(self):
def ratio(self) -> float:
return self.output_rate / self.input_rate / self.speed

def send(self, audio):
def send(self, audio: npt.NDArray[np.int16]) -> None:
# Resample to output rate
data = self.resampler.process(audio, self.ratio).astype(np.int16)
data = self.resampler.process(audio, self.ratio)
# Loop over data blocks
while True:
# Write the current buffer
Expand All @@ -47,15 +64,17 @@ def send(self, audio):
data = data[stop - self.offset :]
self.offset = 0

def stream_callback(self, output_buffer, *args):
def stream_callback(self, output_buffer: npt.NDArray[np.int16], *_: object) -> None:
try:
output_buffer[:] = self.queue.get_nowait()
except Empty:
output_buffer.fill(0)


@contextmanager
def audio_player(console, speed_factor=1.0):
def audio_player(
console: Console, speed_factor: float = 1.0
) -> Iterator[Optional[AudioOut]]:
# Perform late imports
# Those can fail if a linux machine doesn't have portaudio or libsamplerate
# installed
Expand All @@ -77,5 +96,7 @@ def audio_player(console, speed_factor=1.0):


@contextmanager
def no_audio(console, speed_factor=1.0):
def no_audio(
console: Console, speed_factor: float = 1.0
) -> Iterator[Optional[AudioOut]]:
yield None
15 changes: 11 additions & 4 deletions gambaterm/colors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import os
import time
from enum import IntEnum
from typing import Optional

from prompt_toolkit.application import AppSession

BASIC_TERMINALS = [
"screen",
Expand All @@ -23,15 +26,19 @@ class ColorMode(IntEnum):
HAS_24_BIT_COLOR = 4


def detect_local_color_mode(app_session, environ=None, timeout=0.1):
def detect_local_color_mode(
app_session: AppSession,
environ: Optional[dict[str, str]] = None,
timeout: float = 0.1,
) -> ColorMode:
if detect_true_color_support(app_session, timeout):
return ColorMode.HAS_24_BIT_COLOR
if environ is None:
environ = os.environ
environ = dict(os.environ)
return detect_color_mode(environ)


def detect_color_mode(env):
def detect_color_mode(env: dict[str, str]) -> ColorMode:
# Extract interesting variables
term = env.get("TERM", "").lower()
colorterm = env.get("COLORTERM", "").lower()
Expand Down Expand Up @@ -63,7 +70,7 @@ def detect_color_mode(env):
return ColorMode.NO_COLOR


def detect_true_color_support(app_session, timeout=0.1):
def detect_true_color_support(app_session: AppSession, timeout: float = 0.1) -> bool:
# Set unlikely RGB value
app_session.output.write_raw("\033[48:2:1:2:3m")
# Query current configuration using a DECRQSS request
Expand Down
47 changes: 29 additions & 18 deletions gambaterm/console.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import argparse
import tempfile
from enum import IntEnum
from typing import Optional, Callable

import numpy as np
import numpy.typing as npt
Expand All @@ -14,40 +15,50 @@ class Console:
FPS: float = NotImplemented
TICKS_IN_FRAME: int = NotImplemented

class Input(IntEnum):
A = 0x01
B = 0x02
SELECT = 0x04
START = 0x08
RIGHT = 0x10
LEFT = 0x20
UP = 0x40
DOWN = 0x80

romfile: str

@classmethod
def add_console_arguments(cls, parser: argparse.ArgumentParser):
def add_console_arguments(cls, parser: argparse.ArgumentParser) -> None:
pass

def __init__(self, args: argparse.Namespace):
pass
self.romfile = args.romfile

def set_input(self, value: int):
def set_input(self, value: set["Console.Input"]) -> None:
pass

def advance_one_frame(
self, video: npt.NDArray[np.uint32], audio: npt.NDArray[np.int16]
):
) -> tuple[int, int]:
raise NotImplementedError


# Type Alias
InputGetter = Callable[[], set[Console.Input]]


class GameboyColor(Console):
WIDTH: int = 160
HEIGHT: int = 144
FPS: float = 59.727500569606
TICKS_IN_FRAME: int = 35112

class Input(IntEnum):
A = 0x01
B = 0x02
SELECT = 0x04
START = 0x08
RIGHT = 0x10
LEFT = 0x20
UP = 0x40
DOWN = 0x80
gb: GB
force_gameboy: bool
save_directory: Optional[str]

@classmethod
def add_console_arguments(cls, parser: argparse.ArgumentParser):
def add_console_arguments(cls, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--force-gameboy",
"--fg",
Expand All @@ -56,8 +67,8 @@ def add_console_arguments(cls, parser: argparse.ArgumentParser):
)

def __init__(self, args: argparse.Namespace):
super().__init__(args)
self.gb = GB()
self.romfile = args.romfile
self.force_gameboy = args.force_gameboy
self.save_directory = (
tempfile.mkdtemp() if args.input_file is not None else None
Expand All @@ -74,10 +85,10 @@ def __init__(self, args: argparse.Namespace):
open(self.romfile).close()
raise RuntimeError(return_code)

def set_input(self, value: int):
self.gb.set_input(value)
def set_input(self, input_set: set[Console.Input]) -> None:
self.gb.set_input(sum(input_set))

def advance_one_frame(
self, video: npt.NDArray[np.uint32], audio: npt.NDArray[np.int16]
):
) -> tuple[int, int]:
return self.gb.run_for(video, self.WIDTH, audio, self.TICKS_IN_FRAME)
30 changes: 19 additions & 11 deletions gambaterm/controller_input.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import os
from typing import Callable, ContextManager, Iterator
from contextlib import contextmanager

from .console import Console, InputGetter

def get_controller_mapping(console):

def get_controller_mapping(console: Console) -> dict[str, Console.Input]:
return {
# Directions
"A1-": console.Input.UP,
Expand All @@ -27,7 +30,9 @@ def get_controller_mapping(console):


@contextmanager
def pygame_button_pressed_context(deadzone=0.4):
def pygame_button_pressed_context(
deadzone: float = 0.4,
) -> Iterator[Callable[[], set[str]]]:
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1"

try:
Expand All @@ -45,12 +50,12 @@ def pygame_button_pressed_context(deadzone=0.4):
pygame.joystick.init()
joystick = None

def get_pressed():
def get_pressed() -> set[str]:
nonlocal joystick
pygame.event.get()
if pygame.joystick.get_count() == 0:
joystick = None
return {}
return set()
if joystick is None:
joystick = pygame.joystick.Joystick(0)
pressed = {
Expand Down Expand Up @@ -80,21 +85,24 @@ def get_pressed():


@contextmanager
def console_input_from_controller_context(console):
def console_input_from_controller_context(console: Console) -> Iterator[InputGetter]:
controller_mapping = get_controller_mapping(console)

def get_gb_input():
value = 0
for keysym in joystick_get_pressed():
value |= controller_mapping.get(keysym, 0)
return value
def get_gb_input() -> set[Console.Input]:
return {
controller_mapping[keysym]
for keysym in joystick_get_pressed()
if keysym in controller_mapping
}

with pygame_button_pressed_context() as joystick_get_pressed:
yield get_gb_input


@contextmanager
def combine_console_input_from_controller_context(console, context):
def combine_console_input_from_controller_context(
console: Console, context: ContextManager[InputGetter]
) -> Iterator[InputGetter]:
with context as getter1:
with console_input_from_controller_context(console) as getter2:
yield lambda: getter1() | getter2()
Loading

0 comments on commit 74f7380

Please sign in to comment.