Add launcher script for microphone visualizer with gamepad support
This commit is contained in:
+120
-20
@@ -31,8 +31,18 @@ def list_capture_devices():
|
||||
|
||||
|
||||
class MicVisualizer:
|
||||
def __init__(self, width, height, device_index=None, history_seconds=1.5, sample_rate=48000, buffer_samples=1024):
|
||||
if sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO | sdl2.SDL_INIT_AUDIO) != 0:
|
||||
def __init__(
|
||||
self,
|
||||
width,
|
||||
height,
|
||||
device_index=None,
|
||||
history_seconds=1.5,
|
||||
sample_rate=48000,
|
||||
buffer_samples=1024,
|
||||
fullscreen=False,
|
||||
hide_cursor=False,
|
||||
):
|
||||
if sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO | sdl2.SDL_INIT_AUDIO | sdl2.SDL_INIT_JOYSTICK | sdl2.SDL_INIT_GAMECONTROLLER) != 0:
|
||||
raise RuntimeError(_decode_sdl_string(sdl2.SDL_GetError()) or "Failed to initialize SDL")
|
||||
|
||||
self.width = width
|
||||
@@ -42,30 +52,62 @@ class MicVisualizer:
|
||||
self.peak = 0.0
|
||||
self.history_seconds = history_seconds
|
||||
self.title_frame_counter = 0
|
||||
self.fullscreen = fullscreen
|
||||
self.game_controller = None
|
||||
self.joystick = None
|
||||
self.capture_error = None
|
||||
self.audio_device = 0
|
||||
self.sample_rate = max(8000, int(sample_rate))
|
||||
self.channels = 1
|
||||
self.sample_width = ctypes.sizeof(ctypes.c_int16)
|
||||
self.bytes_per_frame = self.sample_width * self.channels
|
||||
|
||||
self.window = sdl2.ext.Window("Mic Visualizer", size=(self.width, self.height))
|
||||
window_flags = sdl2.SDL_WINDOW_FULLSCREEN_DESKTOP if self.fullscreen else 0
|
||||
self.window = sdl2.ext.Window("Mic Visualizer", size=(self.width, self.height), flags=window_flags)
|
||||
self.window.show()
|
||||
self.renderer = sdl2.ext.Renderer(
|
||||
self.window,
|
||||
flags=sdl2.SDL_RENDERER_ACCELERATED | sdl2.SDL_RENDERER_PRESENTVSYNC,
|
||||
)
|
||||
self.sdl_renderer = self.renderer.sdlrenderer
|
||||
self.sprite_factory = sdl2.ext.SpriteFactory(renderer=self.renderer)
|
||||
self.font = sdl2.ext.FontManager(font_path="assets/decterm.ttf", size=20)
|
||||
if hide_cursor:
|
||||
sdl2.SDL_ShowCursor(sdl2.SDL_DISABLE)
|
||||
|
||||
self.device_name, obtained = self._open_capture_device(
|
||||
device_index=device_index,
|
||||
sample_rate=sample_rate,
|
||||
buffer_samples=buffer_samples,
|
||||
)
|
||||
self.sample_rate = int(obtained.freq)
|
||||
self.channels = int(obtained.channels)
|
||||
self.sample_width = ctypes.sizeof(ctypes.c_int16)
|
||||
self.bytes_per_frame = self.sample_width * self.channels
|
||||
self._open_input_device()
|
||||
self.device_name = "No capture device"
|
||||
try:
|
||||
self.device_name, obtained = self._open_capture_device(
|
||||
device_index=device_index,
|
||||
sample_rate=sample_rate,
|
||||
buffer_samples=buffer_samples,
|
||||
)
|
||||
self.sample_rate = int(obtained.freq)
|
||||
self.channels = int(obtained.channels)
|
||||
self.bytes_per_frame = self.sample_width * self.channels
|
||||
except RuntimeError as exc:
|
||||
self.capture_error = str(exc)
|
||||
self.waveform = np.zeros(max(512, int(self.sample_rate * self.history_seconds)), dtype=np.float32)
|
||||
self.spectrum = np.zeros(64, dtype=np.float32)
|
||||
|
||||
sdl2.SDL_PauseAudioDevice(self.audio_device, 0)
|
||||
if self.audio_device:
|
||||
sdl2.SDL_PauseAudioDevice(self.audio_device, 0)
|
||||
self._update_title(force=True)
|
||||
|
||||
def _open_input_device(self):
|
||||
joystick_count = int(sdl2.SDL_NumJoysticks())
|
||||
if joystick_count <= 0:
|
||||
return
|
||||
if hasattr(sdl2, "SDL_IsGameController") and sdl2.SDL_IsGameController(0):
|
||||
controller = sdl2.SDL_GameControllerOpen(0)
|
||||
if controller:
|
||||
self.game_controller = controller
|
||||
return
|
||||
joystick = sdl2.SDL_JoystickOpen(0)
|
||||
if joystick:
|
||||
self.joystick = joystick
|
||||
|
||||
def _open_capture_device(self, device_index, sample_rate, buffer_samples):
|
||||
requested_name = None
|
||||
if device_index is not None:
|
||||
@@ -74,13 +116,19 @@ class MicVisualizer:
|
||||
raise ValueError(f"Capture device index {device_index} out of range")
|
||||
requested_name = available[device_index]
|
||||
|
||||
desired = sdl2.SDL_AudioSpec()
|
||||
desired.freq = int(sample_rate)
|
||||
desired.format = sdl2.AUDIO_S16SYS
|
||||
desired.channels = 1
|
||||
desired.samples = int(buffer_samples)
|
||||
desired = sdl2.SDL_AudioSpec(
|
||||
freq=int(sample_rate),
|
||||
aformat=sdl2.AUDIO_S16SYS,
|
||||
channels=1,
|
||||
samples=int(buffer_samples),
|
||||
)
|
||||
|
||||
obtained = sdl2.SDL_AudioSpec()
|
||||
obtained = sdl2.SDL_AudioSpec(
|
||||
freq=int(sample_rate),
|
||||
aformat=sdl2.AUDIO_S16SYS,
|
||||
channels=1,
|
||||
samples=int(buffer_samples),
|
||||
)
|
||||
requested_name_bytes = requested_name.encode("utf-8") if requested_name else None
|
||||
allowed_changes = sdl2.SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | sdl2.SDL_AUDIO_ALLOW_CHANNELS_CHANGE
|
||||
self.audio_device = sdl2.SDL_OpenAudioDevice(requested_name_bytes, 1, desired, obtained, allowed_changes)
|
||||
@@ -126,6 +174,11 @@ class MicVisualizer:
|
||||
self.spectrum = self.spectrum * 0.65 + reduced * 0.35
|
||||
|
||||
def _pull_audio(self):
|
||||
if not self.audio_device:
|
||||
self.level *= 0.96
|
||||
self.peak *= 0.94
|
||||
self.spectrum *= 0.97
|
||||
return
|
||||
queued = int(sdl2.SDL_GetQueuedAudioSize(self.audio_device))
|
||||
if queued < self.bytes_per_frame:
|
||||
self.level *= 0.96
|
||||
@@ -239,9 +292,34 @@ class MicVisualizer:
|
||||
self.title_frame_counter += 1
|
||||
level_percent = int(round(min(1.0, self.level * 4.0) * 100))
|
||||
peak_percent = int(round(min(1.0, self.peak) * 100))
|
||||
title = f"Mic Visualizer | {self.device_name} | level {level_percent}% | peak {peak_percent}% | Esc quits"
|
||||
if self.capture_error:
|
||||
title = f"Mic Visualizer | {self.capture_error} | Esc/Q/Start/B quits"
|
||||
else:
|
||||
title = (
|
||||
f"Mic Visualizer | {self.device_name} | level {level_percent}% | peak {peak_percent}%"
|
||||
" | Esc/Q/Start/B quits"
|
||||
)
|
||||
sdl2.SDL_SetWindowTitle(self.window.window, title.encode("utf-8"))
|
||||
|
||||
def _draw_status_text(self):
|
||||
lines = []
|
||||
if self.capture_error:
|
||||
lines.append("No microphone capture device detected")
|
||||
lines.append(self.capture_error)
|
||||
else:
|
||||
lines.append(f"Input: {self.device_name}")
|
||||
lines.append(f"Level: {int(round(min(1.0, self.level * 4.0) * 100))}%")
|
||||
lines.append("Esc / Q / B / Start to quit")
|
||||
|
||||
y = 18
|
||||
for line in lines:
|
||||
sprite = self.sprite_factory.from_text(line, color=sdl2.ext.Color(230, 238, 248), fontmanager=self.font)
|
||||
bg = sdl2.SDL_Rect(14, y - 4, sprite.size[0] + 12, sprite.size[1] + 8)
|
||||
self._set_color((10, 14, 22, 220))
|
||||
sdl2.SDL_RenderFillRect(self.sdl_renderer, ctypes.byref(bg))
|
||||
self.renderer.copy(sprite, dstrect=sdl2.SDL_Rect(20, y, *sprite.size))
|
||||
y += sprite.size[1] + 12
|
||||
|
||||
def _handle_events(self):
|
||||
for event in sdl2.ext.get_events():
|
||||
if event.type == sdl2.SDL_QUIT:
|
||||
@@ -249,12 +327,24 @@ class MicVisualizer:
|
||||
elif event.type == sdl2.SDL_KEYDOWN:
|
||||
if event.key.keysym.sym in (sdl2.SDLK_ESCAPE, sdl2.SDLK_q):
|
||||
self.running = False
|
||||
elif hasattr(sdl2, "SDL_CONTROLLERBUTTONDOWN") and event.type == sdl2.SDL_CONTROLLERBUTTONDOWN:
|
||||
if event.cbutton.button in (
|
||||
sdl2.SDL_CONTROLLER_BUTTON_B,
|
||||
sdl2.SDL_CONTROLLER_BUTTON_START,
|
||||
sdl2.SDL_CONTROLLER_BUTTON_BACK,
|
||||
sdl2.SDL_CONTROLLER_BUTTON_GUIDE,
|
||||
):
|
||||
self.running = False
|
||||
elif event.type == sdl2.SDL_JOYBUTTONDOWN:
|
||||
if event.jbutton.button in (0, 1, 6, 7, 8, 9):
|
||||
self.running = False
|
||||
|
||||
def render(self):
|
||||
self._draw_background()
|
||||
self._draw_waveform()
|
||||
self._draw_spectrum()
|
||||
self._draw_level_meter()
|
||||
self._draw_status_text()
|
||||
self.renderer.present()
|
||||
self._update_title()
|
||||
|
||||
@@ -273,6 +363,12 @@ class MicVisualizer:
|
||||
sdl2.SDL_ClearQueuedAudio(self.audio_device)
|
||||
sdl2.SDL_CloseAudioDevice(self.audio_device)
|
||||
self.audio_device = 0
|
||||
if self.game_controller is not None:
|
||||
sdl2.SDL_GameControllerClose(self.game_controller)
|
||||
self.game_controller = None
|
||||
if self.joystick is not None:
|
||||
sdl2.SDL_JoystickClose(self.joystick)
|
||||
self.joystick = None
|
||||
sdl2.SDL_Quit()
|
||||
|
||||
|
||||
@@ -285,6 +381,8 @@ def parse_args():
|
||||
parser.add_argument("--history-seconds", type=float, default=1.5, help="Seconds of waveform history to keep on screen")
|
||||
parser.add_argument("--sample-rate", type=int, default=48000, help="Preferred microphone sample rate")
|
||||
parser.add_argument("--buffer-samples", type=int, default=1024, help="Requested SDL capture buffer size in samples")
|
||||
parser.add_argument("--fullscreen", action="store_true", help="Open in fullscreen desktop mode")
|
||||
parser.add_argument("--hide-cursor", action="store_true", help="Hide the mouse cursor while visualizing")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -306,6 +404,8 @@ def main():
|
||||
history_seconds=max(0.25, float(args.history_seconds)),
|
||||
sample_rate=max(8000, int(args.sample_rate)),
|
||||
buffer_samples=max(128, int(args.buffer_samples)),
|
||||
fullscreen=args.fullscreen,
|
||||
hide_cursor=args.hide_cursor,
|
||||
)
|
||||
visualizer.run()
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user