diff --git a/src/cool sample 2.mp3 b/src/cool sample 2.mp3 deleted file mode 100644 index a1bc816..0000000 Binary files a/src/cool sample 2.mp3 and /dev/null differ diff --git a/src/cool sample.mp3 b/src/cool sample.mp3 deleted file mode 100644 index 1f0dd23..0000000 Binary files a/src/cool sample.mp3 and /dev/null differ diff --git a/src/output.mp3 b/src/output.mp3 deleted file mode 100644 index e69de29..0000000 diff --git a/src/settings_store.py b/src/settings_store.py index b696411..edb0da8 100644 --- a/src/settings_store.py +++ b/src/settings_store.py @@ -20,7 +20,7 @@ class ConfigHandler: return config_dir - def get(self, section: str, option: str): + def get(self, section: str, option: str) -> str: if not self.config.has_section(section): self.config.add_section(section) if not self.config.has_option(section, option): @@ -53,6 +53,10 @@ class ConfigHandler: } self.config["appearance"] = { "colour_theme": "textual-dark" + }, + self.config["audio"] = { + "output_device": "None", + "input_device": "None" } with open(self.config_dir / "config.ini", "w") as configfile: diff --git a/src/song_player.py b/src/song_player.py index 9cbf2fd..b811494 100644 --- a/src/song_player.py +++ b/src/song_player.py @@ -88,11 +88,26 @@ class SongPlayer: if not self.stream.closed: self.stream.close() + # figure out which device we're using + devices = sd.query_devices() + device_index = None + for device in devices: + if device["name"] == self.timeline.app.config_handler.get("audio", "output_device"): + device_index = device["index"] + + # something horrible has happened to our user config lmao + # not even the config manager could find a device, idek + # how that would happen. + if not device_index: + self.pause() + self.timeline.app.notify(f"Failed to find the output device you have specified in settings, please check your config.", title="Error while playing song", severity="error") + return False + self.stream = sd.OutputStream( channels=self.audio.shape[1] if self.audio.ndim > 1 else 1, callback=self.play_callback, - blocksize=256, - device=7 + blocksize=int(self.timeline.app.config_handler.get("audio", "block_size")), + device=device_index ) self.stream.start() diff --git a/src/ui/screens/settings.py b/src/ui/screens/settings.py index 19d81bc..e7c5714 100644 --- a/src/ui/screens/settings.py +++ b/src/ui/screens/settings.py @@ -3,6 +3,10 @@ from textual.widgets import Label, Select, TabbedContent, TabPane, Switch, Input from textual.containers import Vertical, HorizontalGroup, VerticalGroup, VerticalScroll from textual.binding import Binding +from textual_slider import Slider + +import sounddevice as sd + class SettingsScreen(ModalScreen): border_title = "Settings" @@ -30,6 +34,12 @@ class SettingsScreen(ModalScreen): margin-top: 2; text-style: bold; } + + #block-size-text { + width: 5; + height: 3; + content-align: left middle; + } .setting { padding: 0 2; @@ -52,8 +62,8 @@ class SettingsScreen(ModalScreen): margin-right: 1; } - Select, Input { - max-width: 30; + Select, Input, Slider { + max-width: 35; } } } @@ -72,7 +82,16 @@ class SettingsScreen(ModalScreen): def on_select_changed(self, event: Select.Changed): if event.select.id == "colour-theme": self.app.theme = event.value - self.app.config_handler.set("appearance", "colour_theme", str(event.value)) + self.app.config_handler.set("appearance", "colour_theme", event.value) + elif event.select.id == "output-device": + self.app.config_handler.set("audio", "output_device", event.value) + elif event.select.id == "input-device": + self.app.config_handler.set("audio", "input_device", event.value) + + def on_slider_changed(self, event: Slider.Changed): + if event.slider.id == "block-size": + self.query_one("#block-size-text").update(str(event.value)) + self.app.config_handler.set("audio", "block_size", str(event.value)) def compose(self): with Vertical(id="window") as window: @@ -90,4 +109,47 @@ class SettingsScreen(ModalScreen): allow_blank=False, id="colour-theme", value=self.app.theme + ) + with TabPane("Audio"): + with VerticalScroll(): + with HorizontalGroup(classes="setting"): + with VerticalGroup(): + yield Label("Output Device", classes="setting-name") + yield Label("The device that audio will be outputted to. If a device is not shown, then [i]sounddevice[/i] thinks it has 0 output channels.", classes="setting-desc") + + devices: list[str] = [device["name"] for device in sd.query_devices() if device["max_output_channels"] > 0] + output_device_name = self.app.config_handler.get("audio", "output_device") + + yield Select.from_values( + devices, + allow_blank=False, + id="output-device", + value=output_device_name if output_device_name in devices else devices[0] + ) + with HorizontalGroup(classes="setting"): + with VerticalGroup(): + yield Label("Input Device", classes="setting-name") + yield Label("The device that will be used for recording audio. If a device is not shown, then [i]sounddevice[/i] thinks it has 0 input channels. (this setting is unused for now)", classes="setting-desc") + + devices: list[str] = [device["name"] for device in sd.query_devices() if device["max_input_channels"] > 0] + input_device_name = self.app.config_handler.get("audio", "input_device") + + yield Select.from_values( + devices, + allow_blank=False, + id="input-device", + value=input_device_name if input_device_name in devices else devices[0] + ) + with HorizontalGroup(classes="setting"): + with VerticalGroup(): + yield Label("Block Size", classes="setting-name") + yield Label("Block size of audio playback. Higher block size will reduce stutters or cut outs, but will also lead to higher CPU strain.", classes="setting-desc") + + yield Static("N/A", id="block-size-text") + yield Slider( + 16, + 2048, + step=16, + value=int(self.app.config_handler.get("audio", "block_size")), + id="block-size" ) \ No newline at end of file