Skip to content

Commit b2a531a

Browse files
committed
use standard logging interface, only
1 parent aa702d6 commit b2a531a

4 files changed

Lines changed: 121 additions & 49 deletions

File tree

gambaterm/audio.py

Lines changed: 53 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import atexit
4+
import logging
45
import os
56
from typing import Generator, Iterator, TYPE_CHECKING
67
from contextlib import contextmanager
@@ -16,6 +17,8 @@
1617
import miniaudio
1718
import samplerate
1819

20+
logger = logging.getLogger(__name__)
21+
1922

2023
class AudioOut:
2124
output_rate: float = 48000.0 # Hz
@@ -66,24 +69,22 @@ def __init__(
6669
# Diagnostics variables
6770
self._underruns = 0
6871
self._overruns = 0
69-
self._diag_enabled = bool(
70-
os.environ.get("GAMBATERM_AUDIO_CSV")
71-
or os.environ.get("GAMBATERM_AUDIO_LOG")
72-
)
73-
if self._diag_enabled:
72+
self._frame_num = 0
73+
self._csv_enabled = bool(os.environ.get("GAMBATERM_AUDIO_CSV"))
74+
self._diag_fill_min = 1.0
75+
self._diag_ratio_min = self.nominal_sampling_ratio
76+
self._diag_ratio_max = self.nominal_sampling_ratio
77+
if self._csv_enabled:
7478
self._diag_frames: list[dict] = []
75-
self._diag_frame_num = 0
76-
self._diag_fill_min = 1.0
77-
self._diag_ratio_min = self.nominal_sampling_ratio
78-
self._diag_ratio_max = self.nominal_sampling_ratio
79-
atexit.register(self._dump_audio_stats)
79+
atexit.register(self._dump_csv)
80+
atexit.register(self._log_summary)
8081

8182
# Controller configuration
8283
self.correction_min = 1 - self.correction_clamp
8384
self.correction_max = 1 + self.correction_clamp
8485

85-
# Batch variable-length emulator output to avoid starving the
86-
# ring buffer as runFor() sometimes returns partial frames.
86+
# Batch the variable-length emulator audio output, avoids starving the ring buffer, runFor()
87+
# sometimes returns partial frames!
8788
self._acc_buf: npt.NDArray[np.float32] = np.empty((0, 2), dtype=np.float32)
8889

8990
# Controller state
@@ -107,16 +108,7 @@ def start(self) -> miniaudio.PlaybackDevice:
107108
device.start(stream)
108109
return device
109110

110-
def _dump_audio_stats(self) -> None:
111-
if (audio_log := os.environ.get("GAMBATERM_AUDIO_LOG")):
112-
with open(audio_log, "w") as fout:
113-
fout.write(f"underruns={self._underruns}\n")
114-
fout.write(f"overruns={self._overruns}\n")
115-
fout.write(f"fill_min={self._diag_fill_min:.4f}\n")
116-
fout.write(f"ratio_min={self._diag_ratio_min:.8f}\n")
117-
fout.write(f"ratio_max={self._diag_ratio_max:.8f}\n")
118-
_rng = self._diag_ratio_max - self._diag_ratio_min
119-
fout.write(f"ratio_range={_rng:.8f}\n")
111+
def _dump_csv(self) -> None:
120112
if (diag_csv := os.environ.get("GAMBATERM_AUDIO_CSV")):
121113
with open(diag_csv, "w") as fout:
122114
fout.write("frame,input,acc,proc,output,fill\n")
@@ -126,6 +118,15 @@ def _dump_audio_stats(self) -> None:
126118
f"{_df['proc']},{_df['output']},{_df['fill']:.4f}\n"
127119
)
128120

121+
def _log_summary(self) -> None:
122+
logger.debug("Audio stats: underruns=%d overruns=%d "
123+
"fill_min=%.4f ratio_min=%.8f ratio_max=%.8f "
124+
"ratio_range=%.8f",
125+
self._underruns, self._overruns,
126+
self._diag_fill_min, self._diag_ratio_min,
127+
self._diag_ratio_max,
128+
self._diag_ratio_max - self._diag_ratio_min)
129+
129130
@property
130131
def fill_fraction(self) -> float:
131132
# Ring buffer fill ratio (0-1.0)
@@ -162,45 +163,45 @@ def adapt_sample_rate(self) -> None:
162163
self._diag_track_ratio()
163164

164165
def _diag_record_skip(self, input_len: int, acc_len: int) -> None:
165-
if not self._diag_enabled:
166-
return
167166
fill = self.fill_fraction
168167
self._diag_fill_min = min(self._diag_fill_min, fill)
169-
self._diag_frames.append({
170-
"frame": self._diag_frame_num,
171-
"input": input_len,
172-
"acc": acc_len,
173-
"proc": 0,
174-
"output": 0,
175-
"fill": fill,
176-
})
177-
self._diag_frame_num += 1
168+
frame = self._frame_num
169+
self._frame_num += 1
170+
logger.debug("skip frame=%d input=%d acc=%d fill=%.4f",
171+
frame, input_len, acc_len, fill)
172+
if self._csv_enabled:
173+
self._diag_frames.append({
174+
"frame": frame,
175+
"input": input_len,
176+
"acc": acc_len,
177+
"proc": 0,
178+
"output": 0,
179+
"fill": fill,
180+
})
178181

179182
def _diag_record_process(
180183
self, input_len: int, acc_len: int, output_len: int,
181184
) -> None:
182-
if not self._diag_enabled:
183-
return
184185
fill = self.fill_fraction
185186
self._diag_fill_min = min(self._diag_fill_min, fill)
186-
self._diag_frames.append({
187-
"frame": self._diag_frame_num,
188-
"input": input_len,
189-
"acc": acc_len,
190-
"proc": 1,
191-
"output": output_len,
192-
"fill": fill,
193-
})
194-
self._diag_frame_num += 1
187+
frame = self._frame_num
188+
self._frame_num += 1
189+
logger.debug("process frame=%d input=%d acc=%d output=%d fill=%.4f",
190+
frame, input_len, acc_len, output_len, fill)
191+
if self._csv_enabled:
192+
self._diag_frames.append({
193+
"frame": frame,
194+
"input": input_len,
195+
"acc": acc_len,
196+
"proc": 1,
197+
"output": output_len,
198+
"fill": fill,
199+
})
195200

196201
def _diag_track_fill(self) -> None:
197-
if not self._diag_enabled:
198-
return
199202
self._diag_fill_min = min(self._diag_fill_min, self.fill_fraction)
200203

201204
def _diag_track_ratio(self) -> None:
202-
if not self._diag_enabled:
203-
return
204205
self._diag_ratio_min = min(self._diag_ratio_min, self.sampling_ratio)
205206
self._diag_ratio_max = max(self._diag_ratio_max, self.sampling_ratio)
206207

@@ -246,6 +247,8 @@ def send(self, console: Console, audio: npt.NDArray[np.int16]) -> None:
246247
# Drop excess frames if we're overrun
247248
if frames > space:
248249
self._overruns += 1
250+
logger.warning("Audio overrun: dropping %d of %d frames (fill=%.2f)",
251+
frames - space, frames, self.fill_fraction)
249252
resampled = resampled[:space]
250253
frames = space
251254

@@ -317,6 +320,8 @@ def _audio_stream(self) -> Generator[bytes, int, None]:
317320
# Log if we're underrunning
318321
if read_size < required_frames:
319322
self._underruns += 1
323+
logger.warning("Audio underrun: requested %d, got %d (fill=%.2f)",
324+
required_frames, read_size, self.fill_fraction)
320325

321326
# Send audio to output and get next required frames
322327
required_frames = yield result.tobytes()

gambaterm/main.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
from __future__ import annotations
33

44
import time
5+
import logging
56
import argparse
67
from pathlib import Path
7-
from typing import ContextManager, TYPE_CHECKING
8+
from typing import Any, ContextManager, Optional, TYPE_CHECKING
89
import dataclasses
910
from dataclasses import dataclass, field
1011

@@ -23,6 +24,45 @@
2324
if TYPE_CHECKING:
2425
from typing import Self
2526

27+
_DEFAULT_LOGFMT = " ".join(("%(levelname)s", "%(filename)s:%(lineno)d", "%(message)s"))
28+
29+
30+
def make_logger(
31+
name: str,
32+
loglevel: str = "info",
33+
logfile: Optional[str] = None,
34+
logfmt: str = _DEFAULT_LOGFMT,
35+
filemode: str = "a",
36+
) -> logging.Logger:
37+
"""Create and return a configured logger (following telnetlib3 pattern)."""
38+
lvl = getattr(logging, loglevel.upper(), None)
39+
if lvl is None:
40+
lvl = logging.getLevelName(loglevel.upper())
41+
_cfg: dict[str, Any] = {"format": logfmt}
42+
if logfile:
43+
_cfg["filename"] = logfile
44+
_cfg["filemode"] = filemode
45+
logging.basicConfig(**_cfg)
46+
logging.getLogger().setLevel(lvl)
47+
logging.getLogger(name).setLevel(lvl)
48+
return logging.getLogger(name)
49+
50+
51+
def add_logging_arguments(parser: argparse.ArgumentParser) -> None:
52+
parser.add_argument(
53+
"--logfile", default=None, help="File path for log output (default: stderr)"
54+
)
55+
parser.add_argument(
56+
"--loglevel",
57+
default="warn",
58+
help="Logging level: debug, info, warn, error (default: warn)",
59+
)
60+
parser.add_argument(
61+
"--logfmt",
62+
default=_DEFAULT_LOGFMT,
63+
help="Log format string (default: LEVEL file:lineno message)",
64+
)
65+
2666

2767
@dataclass
2868
class AppConfig:
@@ -151,11 +191,18 @@ def main(
151191
add_base_arguments(parser)
152192
add_input_file_arguments(parser)
153193
add_tuning_arguments(parser)
194+
add_logging_arguments(parser)
154195
add_local_only_arguments(parser)
155196
console_cls.add_console_arguments(parser)
156197

157198
# Parse arguments
158199
namespace = parser.parse_args(parser_args)
200+
make_logger(
201+
__name__,
202+
loglevel=getattr(namespace, "loglevel", "warn"),
203+
logfile=getattr(namespace, "logfile", None),
204+
logfmt=getattr(namespace, "logfmt", _DEFAULT_LOGFMT),
205+
)
159206
disable_audio = getattr(namespace, "disable_audio", False)
160207
args = LocalAppConfig.from_namespace(namespace)
161208

gambaterm/ssh.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
add_base_arguments,
3939
add_input_file_arguments,
4040
add_tuning_arguments,
41+
add_logging_arguments,
42+
make_logger,
43+
_DEFAULT_LOGFMT,
4144
AppConfig,
4245
)
4346
from .console import Console, GameboyColor
@@ -533,6 +536,7 @@ def main(
533536
add_base_arguments(parser)
534537
add_input_file_arguments(parser)
535538
add_tuning_arguments(parser)
539+
add_logging_arguments(parser)
536540
console_cls.add_console_arguments(parser)
537541
parser.add_argument(
538542
"--bind",
@@ -570,6 +574,12 @@ def main(
570574

571575
# Parse arguments
572576
namespace = parser.parse_args(parser_args)
577+
make_logger(
578+
__name__,
579+
loglevel=getattr(namespace, "loglevel", "warn"),
580+
logfile=getattr(namespace, "logfile", None),
581+
logfmt=getattr(namespace, "logfmt", _DEFAULT_LOGFMT),
582+
)
573583
bind: str = namespace.__dict__.pop("bind")
574584
port: int = namespace.__dict__.pop("port")
575585
password: str = namespace.__dict__.pop("password")

gambaterm/telnet.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
add_base_arguments,
3333
add_input_file_arguments,
3434
add_tuning_arguments,
35+
add_logging_arguments,
36+
make_logger,
37+
_DEFAULT_LOGFMT,
3538
AppConfig,
3639
)
3740
from .console import Console, GameboyColor
@@ -444,6 +447,7 @@ def main(
444447
add_base_arguments(parser)
445448
add_input_file_arguments(parser)
446449
add_tuning_arguments(parser)
450+
add_logging_arguments(parser)
447451
console_cls.add_console_arguments(parser)
448452
parser.add_argument(
449453
"--bind",
@@ -488,6 +492,12 @@ def main(
488492
)
489493

490494
namespace = parser.parse_args(parser_args)
495+
make_logger(
496+
__name__,
497+
loglevel=getattr(namespace, "loglevel", "warn"),
498+
logfile=getattr(namespace, "logfile", None),
499+
logfmt=getattr(namespace, "logfmt", _DEFAULT_LOGFMT),
500+
)
491501
bind: str = namespace.__dict__.pop("bind")
492502
port: int = namespace.__dict__.pop("port")
493503
robot_check: bool = namespace.__dict__.pop("robot_check")

0 commit comments

Comments
 (0)