From a40fcf161cdf8c279c2f623cd565ca408bbb4e12 Mon Sep 17 00:00:00 2001 From: Dragan Date: Wed, 20 Mar 2024 18:51:20 +0100 Subject: [PATCH] Add badges, readme improvements, add alsa buffer frame size, pin playback thread to last cpu core, postinstall improve, thread priority, audio buffer size, use env file for variables --- Cargo.lock | 114 ++++++----- Makefile.toml | 20 +- .../etc/systemd/system/rsplayer.service | 12 +- PKGS/debian/opt/rsplayer/env | 7 + PKGS/maintainer-scripts/postinst | 32 ++- README.md | 53 +++-- rsplayer_api_models/Cargo.toml | 2 +- rsplayer_api_models/src/settings.rs | 58 +++--- rsplayer_backend/Cargo.toml | 21 ++ rsplayer_backend/src/main.rs | 3 +- rsplayer_backend/src/server_warp.rs | 28 +-- rsplayer_hardware/src/audio_device/ak4497.rs | 9 +- rsplayer_playback/Cargo.toml | 2 + rsplayer_playback/src/rsp/mod.rs | 126 +++++++----- rsplayer_playback/src/rsp/output.rs | 77 ++++++-- rsplayer_playback/src/rsp/symphonia.rs | 24 ++- rsplayer_web_ui/src/lib.rs | 10 +- rsplayer_web_ui/src/page/settings.rs | 184 ++++++++++++++---- 18 files changed, 514 insertions(+), 268 deletions(-) create mode 100644 PKGS/debian/opt/rsplayer/env diff --git a/Cargo.lock b/Cargo.lock index 18ec9ab..eb05601 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -505,6 +505,17 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "core_affinity" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622892f5635ce1fc38c8f16dfc938553ed64af482edb5e150bf4caedbfcb2304" +dependencies = [ + "libc", + "num_cpus", + "winapi", +] + [[package]] name = "coreaudio-rs" version = "0.11.3" @@ -600,41 +611,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "darling" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.52", -] - -[[package]] -name = "darling_macro" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.52", -] - [[package]] name = "dasp_sample" version = "0.11.0" @@ -1139,10 +1115,14 @@ dependencies = [ ] [[package]] -name = "ident_case" -version = "1.0.1" +name = "idna" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] [[package]] name = "idna" @@ -1154,6 +1134,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "if_chain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" + [[package]] name = "include-flate" version = "0.2.0" @@ -2138,6 +2124,7 @@ version = "0.1.0" dependencies = [ "anyhow", "api_models", + "core_affinity", "cpal", "env_logger", "log", @@ -2148,6 +2135,7 @@ dependencies = [ "rsplayer_config", "rsplayer_metadata", "symphonia", + "thread-priority", "tokio", "ureq", ] @@ -2484,12 +2472,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strum" version = "0.25.0" @@ -2779,6 +2761,20 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "thread-priority" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a617e9eeeb20448b01a8e2427fb80dfbc9c49d79a1de3b11f25731edbf547e3c" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "libc", + "log", + "rustversion", + "winapi", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -3122,7 +3118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", ] @@ -3150,12 +3146,12 @@ dependencies = [ [[package]] name = "validator" -version = "0.17.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da339118f018cc70ebf01fafc103360528aad53717e4bf311db929cb01cb9345" +checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" dependencies = [ - "idna", - "once_cell", + "idna 0.4.0", + "lazy_static", "regex", "serde", "serde_derive", @@ -3166,16 +3162,28 @@ dependencies = [ [[package]] name = "validator_derive" -version = "0.17.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76e88ea23b8f5e59230bff8a2f03c0ee0054a61d5b8343a38946bcd406fe624c" +checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af" dependencies = [ - "darling", + "if_chain", + "lazy_static", "proc-macro-error", "proc-macro2", "quote", "regex", - "syn 2.0.52", + "syn 1.0.109", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" +dependencies = [ + "proc-macro2", + "syn 1.0.109", ] [[package]] diff --git a/Makefile.toml b/Makefile.toml index 903e8ef..f8abd5b 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -6,7 +6,7 @@ RPI_HOST = "192.168.5.4" TARGET = "aarch64-unknown-linux-gnu" -RELEASE_VERSION = "0.9.6" +RELEASE_VERSION = "0.9.8" RELEASE_BRANCH = "main" [config] @@ -27,6 +27,17 @@ args = [ "--bin", "rsplayer", ] +[tasks.build_debug] +command = "cross" +args = [ + "build", + "--target", + "${TARGET}", + "--package", + "rsplayer", + "--bin", + "rsplayer", +] [tasks.build_ui_release] cwd = "rsplayer_web_ui" command = "cargo" @@ -132,7 +143,7 @@ script = "sudo pkill -9 rsplayer || true" [tasks.run_local] dependencies = ["kill_local"] -env = { "RUST_LOG" = "rsplayer=info,rsplayer_playback=info,warp=info", "RUST_BACKTRACE" = "full", "RSPLAYER_HTTP_PORT" = "8000", "RSPLAYER_HTTPS_PORT" = "8443" } +env = { "RUST_LOG" = "rsplayer=info,rsplayer_playback=info,warp=info", "RUST_BACKTRACE" = "full", "PORT" = "8000", "TLS_PORT" = "8443", "TLS_CERT_PATH" = "self.crt", "TLS_CERT_KEY_PATH" = "self.key" } cwd = ".run" command = "cargo" args = ["run"] @@ -142,6 +153,11 @@ dependencies = ["build_release"] script = [ "rsync -avvP --rsync-path=\"sudo rsync\" target/${TARGET}/release/rsplayer pi@${RPI_HOST}:/usr/bin", ] +[tasks.copy_remote_debug] +dependencies = ["build_debug"] +script = [ + "rsync -avvP --rsync-path=\"sudo rsync\" target/${TARGET}/debug/rsplayer pi@${RPI_HOST}:/usr/bin", +] [tasks.package_deb_local] dependencies = ["build_ui_release", "build_release", "package_deb_release"] diff --git a/PKGS/debian/etc/systemd/system/rsplayer.service b/PKGS/debian/etc/systemd/system/rsplayer.service index d28ad92..3974f92 100644 --- a/PKGS/debian/etc/systemd/system/rsplayer.service +++ b/PKGS/debian/etc/systemd/system/rsplayer.service @@ -5,10 +5,7 @@ Wants=network.target sound.target After=remote-fs.target [Service] -Environment=RUST_BACKTRACE=full -Environment=RSPLAYER_HTTP_PORT=80 -Environment=RSPLAYER_HTTPS_PORT=443 -Environment=RUST_LOG=rsplayer=info,warp=info +EnvironmentFile=/opt/rsplayer/env ExecStart=/usr/bin/rsplayer WorkingDirectory=/opt/rsplayer Restart=always @@ -18,8 +15,11 @@ TimeoutStopSec=5 User=rsplayer LimitRTPRIO=40 LimitRTTIME=infinity -AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_SYS_BOOT -CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SYS_BOOT +AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_SYS_BOOT CAP_SYS_NICE +CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SYS_BOOT CAP_SYS_NICE + +CPUSchedulingPolicy=rr + [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/PKGS/debian/opt/rsplayer/env b/PKGS/debian/opt/rsplayer/env new file mode 100644 index 0000000..411d276 --- /dev/null +++ b/PKGS/debian/opt/rsplayer/env @@ -0,0 +1,7 @@ +PORT=80 +TLS_PORT=443 +TLS_CERT_PATH=/opt/rsplayer/self.crt +TLS_CERT_KEY_PATH=/opt/rsplayer/self.key + +RUST_LOG=rsplayer=info,warp=info + diff --git a/PKGS/maintainer-scripts/postinst b/PKGS/maintainer-scripts/postinst index 0e1649d..7807b56 100755 --- a/PKGS/maintainer-scripts/postinst +++ b/PKGS/maintainer-scripts/postinst @@ -1,29 +1,25 @@ #!/usr/bin/env bash if [ "$1" = "configure" ]; then if [ -z $2 ] - # this is clean install then - echo "No previous version found this is clean install" + # this is clean install + previous_version="0.0.0" + else + # this is upgrade + previous_version=$2 + fi + if dpkg --compare-versions "$previous_version" lt "0.9.1"; then + echo "Upgrading from version $previous_version to 0.9.1" adduser --system --no-create-home --disabled-login rsplayer usermod -a -G audio rsplayer # optional for hardware access - usermod -a -G i2c,gpio,spi,input rsplayer || true - mkdir -p /opt/rsplayer + usermod -a -G i2c,gpio,spi,audio,input rsplayer || true chown -R rsplayer /opt/rsplayer - #this is upgrade - else - echo "Previous version found: $2" - VERSION_TO_COMPARE="0.9.1" - if [ "$(printf '%s\n' "$VERSION_TO_COMPARE" "$2" | sort -V | head -n1)" = "$VERSION_TO_COMPARE" ]; then - echo "$2 is greater than $VERSION_TO_COMPARE migration not needed." - else - echo "$2 is not greater than $VERSION_TO_COMPARE" - adduser --system --no-create-home --disabled-login rsplayer - usermod -a -G audio rsplayer - # optional for hardware access - usermod -a -G i2c,gpio,spi,audio,input rsplayer || true - chown -R rsplayer /opt/rsplayer - fi + fi + if dpkg --compare-versions "$previous_version" lt "0.9.7"; then + echo "Upgrading from version $previous_version to 0.9.7" + chown rsplayer /opt/rsplayer/env + chown rsplayer /opt/rsplayer/self.* fi systemctl daemon-reload systemctl enable rsplayer diff --git a/README.md b/README.md index 77ca8a4..3186020 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +![](https://github.com/ljufa/rsplayer/actions/workflows/ci.yml/badge.svg) +![](https://github.com/ljufa/rsplayer/actions/workflows/cd.yml/badge.svg) +![](https://github.com/ljufa/rsplayer/actions/workflows/docker.yml/badge.svg) +![](https://img.shields.io/github/v/release/ljufa/rsplayer) +![](https://img.shields.io/github/license/ljufa/rsplayer?style=flat-square) +![](https://img.shields.io/badge/PRs-Welcome-brightgreen.svg?style=flat-square) # RSPlayer RSPlayer is open-source music player designed specifically for headless computing environments. It shines on devices like the Raspberry Pi and other Linux-powered Single Board Computers (SBCs). @@ -8,10 +14,29 @@ Under the hood, RSPlayer harnesses the power of the [Symphonia](https://github.c For DIY enthusiasts seeking a customizable, high-performance music player for their projects, RSPlayer is the go-to choice. Its lightweight design and efficient resource usage make it ideal for transforming your Raspberry Pi or other SBCs into a dedicated music station. ### Online demo -> https://rsplayer.dlj.freemyip.com/ -### Detailed documentation -> https://ljufa.github.io/rsplayer/ -## Installation -To install RSPlayer, execute the following script (requires curl): +## Features +- **Low Latency Output**: Direct output to ALSA minimizes latency. +- **Adjustable Playback Thread Priority**: Customize the priority of the playback thread up to a real-time rating of 99 via the settings page. +- **Dedicated CPU Core for Playback**: By default, the playback thread is pinned to a single CPU core for optimized performance. +- **Web UI Remote Control**: Manage your playback remotely with an intuitive web interface. +- **Flexible Volume Control**: Control the volume using software (alsa mixer) or hardware control (Dac chip instuctions via GPIO). +- **Infrared Remote Control**: Use LIRC for convenient control with an infrared remote. +- **Written in Rust**: Enjoy the benefits of minimal dependencies and high performance, thanks to the Rust native implementation. +- **Comprehensive Music Library Management**: Scan, search, and browse your music library and online radio stations with ease. +- **Dynamic Playlists**: Automaticaly create dynamic playlists for personalized listening experiences. + +### Planed features + +- **DSD and DoP Playback**: Implement support for Direct Stream Digital (DSD) and Digital over PCM (DoP) playback for high-quality audio. +- **Expanded Audio Codec Support**: Compatibility with a wider range of audio codecs. +- **Intelligent Dynamic Playlists**: Advanced dynamic playlists that adapt based on user likes or playback counts for a personalized listening experience. +- **Windows Compatibility**: Development of a Windows build to extend platform support. +- **MacOS Compatibility**: Development of a MacOS build to extend platform support. +- **DSP Support**: Integration of Digital Signal Processing (DSP) capabilities for enhanced audio effects and manipulations. +- **Remote file system management**: Ability to mount and use remote file storage (nfs and samba) from UI. +## Installation +To install RSPlayer on debian based linux distro, execute the following script (requires curl): ```bash bash <(curl -s https://raw.githubusercontent.com/ljufa/rsplayer/master/install.sh) ``` @@ -53,15 +78,17 @@ volumes: Once RSPlayer is installed, you can access the web user interface by navigating to http://localhost or the IP address of the machine on which it is installed. From the web user interface, you can finish configuration following steps described [here](https://ljufa.github.io/rsplayer/#/?id=basic-configuration). For minimal working configuration it is required to select *Audio interface*, *PCM device*, *Music directory path* followed by *Update library*. +#### Detailed documentation -> https://ljufa.github.io/rsplayer/ -## Features -* Basic player features: play, next, prev, volume control -* Player controls: play, pause, next, prev, toggle shuffle -* View, manage, search playback queue -* Browse static and dynamic playlists -* Software volume control by Alsa + ## Requirements +* x86(Amd64) or Arm(64 and 32bit) computer with debian based linux distribution. + +## Tested on +* Rpi4 - RpiOS bookworm and bullseye +* Rpi Zero WH - RpiOS bookworm and bullseye +* Various x86_64 laptop/pc with ubuntu and debian based distros -Optionaly with additional hardware devices it provides: +## With additional hardware devices it provides additional features * Hardware volume control by DAC chip * Infrared remote control: Play, Pause, Next, Prev, Volume Up/Down, Poweroff * Volume control using rotary encoder @@ -69,11 +96,7 @@ Optionaly with additional hardware devices it provides: * Switch audio output between speakers and headphones * Change DAC settings: digital filter, gain, sound profile - ## Hardware requirements -Mandatory: -* x86(Amd64) or Arm(64 and 32bit) computer with debian based linux distribution installed. - -Optionaly if you use SBC with Gpio header you can connect and use following devices: +#### Example hardware list for DIY streamer implementation: * Diy friendly AK44xx DAC board i.e. [Diyinhk](https://www.diyinhk.com/shop/audio-kits/), [JLSounds](http://jlsounds.com/products.html) ... * USB to I2S converter board. i.e. [WaveIO](https://luckit.biz/), [Amanero](https://amanero.com/), [JLSounds](http://jlsounds.com/products.html) ... * Infrared Receiver TSOP312xx. i.e. [TSOP31238](https://eu.mouser.com/ProductDetail/Vishay-Semiconductors/TSOP31238?qs=5rGgbCH0pB1jaK4I0GvRsw%3D%3D) diff --git a/rsplayer_api_models/Cargo.toml b/rsplayer_api_models/Cargo.toml index 1bdaca6..6b25500 100644 --- a/rsplayer_api_models/Cargo.toml +++ b/rsplayer_api_models/Cargo.toml @@ -10,7 +10,7 @@ chrono.workspace = true anyhow.workspace = true uuid.workspace = true -validator = { version = "0.17.0", features = ["derive"] } +validator = { version = "0.16.0", features = ["derive"] } regex = "1.10.3" strum = "0.25.0" diff --git a/rsplayer_api_models/src/settings.rs b/rsplayer_api_models/src/settings.rs index 5bae720..6db1256 100644 --- a/rsplayer_api_models/src/settings.rs +++ b/rsplayer_api_models/src/settings.rs @@ -1,13 +1,11 @@ use std::collections::HashMap; -use num_derive::{FromPrimitive, ToPrimitive}; use serde::{Deserialize, Serialize}; -use strum_macros::{EnumIter, EnumString, IntoStaticStr}; use validator::Validate; use crate::common::{AudioCard, CardMixer, FilterType, GainLevel, PcmOutputDevice, VolumeCrtlType}; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Validate)] pub struct Settings { pub volume_ctrl_settings: VolumeControlSettings, pub output_selector_settings: OutputSelectorSettings, @@ -22,20 +20,45 @@ pub struct Settings { #[serde(default)] pub playlist_settings: PlaylistSetting, #[serde(default)] + #[validate] pub rs_player_settings: RsPlayerSettings, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Validate)] pub struct RsPlayerSettings { pub enabled: bool, - pub buffer_size_mb: usize, + + #[serde(default = "input_stream_buffer_size_default_value")] + #[validate(range(min = 1, max = 200))] + pub input_stream_buffer_size_mb: usize, + + #[serde(default = "ring_buffer_size_default_value")] + #[validate(range(min = 100, max = 10000))] + pub ring_buffer_size_ms: usize, + + #[serde(default = "thread_priority_default_value")] + #[validate(range(min = 1, max = 99))] + pub player_threads_priority: u8, + pub alsa_buffer_size: Option, +} +const fn thread_priority_default_value() -> u8 { + 1 +} +const fn ring_buffer_size_default_value() -> usize { + 200 +} +const fn input_stream_buffer_size_default_value() -> usize { + 10 } impl Default for RsPlayerSettings { fn default() -> Self { Self { enabled: true, - buffer_size_mb: 10, + input_stream_buffer_size_mb: 10, + ring_buffer_size_ms: 200, + player_threads_priority: 1, + alsa_buffer_size: None, } } } @@ -55,29 +78,6 @@ pub struct VolumeControlSettings { pub rotary_event_device_path: String, } -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - Serialize, - Deserialize, - FromPrimitive, - ToPrimitive, - EnumString, - EnumIter, - IntoStaticStr, -)] -pub enum AlsaDeviceFormat { - F64, - F32, - S32, - S24, - S24_3, - S16, -} - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Validate)] pub struct MetadataStoreSettings { pub music_directory: String, diff --git a/rsplayer_backend/Cargo.toml b/rsplayer_backend/Cargo.toml index 4175a85..69323ad 100644 --- a/rsplayer_backend/Cargo.toml +++ b/rsplayer_backend/Cargo.toml @@ -40,6 +40,12 @@ assets = [ "opt/rsplayer/", "644", ], + [ + "../PKGS/debian/opt/rsplayer/env", + "opt/rsplayer/", + "644", + ], + ] [package.metadata.deb.variants.aarch64-unknown-linux-gnu] @@ -70,6 +76,11 @@ assets = [ "opt/rsplayer/", "644", ], + [ + "../PKGS/debian/opt/rsplayer/env", + "opt/rsplayer/", + "644", + ], [ "../PKGS/debian/etc/lirc/lircd.conf.d/*", "etc/lirc/lircd.conf.d", @@ -105,6 +116,11 @@ assets = [ "opt/rsplayer/", "644", ], + [ + "../PKGS/debian/opt/rsplayer/env", + "opt/rsplayer/", + "644", + ], [ "../PKGS/debian/etc/lirc/lircd.conf.d/*", "etc/lirc/lircd.conf.d", @@ -134,6 +150,11 @@ assets = [ "opt/rsplayer/", "644", ], + [ + "../PKGS/debian/opt/rsplayer/env", + "opt/rsplayer/", + "644", + ], [ "../PKGS/debian/etc/lirc/lircd.conf.d/*", "etc/lirc/lircd.conf.d", diff --git a/rsplayer_backend/src/main.rs b/rsplayer_backend/src/main.rs index 2cc7336..c570d49 100644 --- a/rsplayer_backend/src/main.rs +++ b/rsplayer_backend/src/main.rs @@ -2,12 +2,14 @@ extern crate env_logger; #[macro_use] extern crate log; + use std::panic; use std::sync::Arc; #[cfg(debug_assertions)] use std::time::Duration; use env_logger::Env; + use tokio::signal::unix::{Signal, SignalKind}; use tokio::sync::broadcast; use tokio::{select, spawn}; @@ -152,7 +154,6 @@ async fn main() { } _ = spawn(http_server_future) => {} - _ = spawn(https_server_future) => {} _ = spawn(websocket_future) => { diff --git a/rsplayer_backend/src/server_warp.rs b/rsplayer_backend/src/server_warp.rs index 81cae21..cedb435 100644 --- a/rsplayer_backend/src/server_warp.rs +++ b/rsplayer_backend/src/server_warp.rs @@ -41,8 +41,8 @@ static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1); type Users = Arc>>>>; type Config = Arc; -type UserCommandSender = tokio::sync::mpsc::Sender; -type SystemCommandSender = tokio::sync::mpsc::Sender; +type UserCommandSender = mpsc::Sender; +type SystemCommandSender = mpsc::Sender; #[derive(RustEmbed)] #[folder = "../rsplayer_web_ui/public"] @@ -133,12 +133,15 @@ pub fn start( } } }; - let http_handle = warp::serve(routes.clone()).run(([0, 0, 0, 0], get_ports().0)); + let ports = get_ports(); + let http_handle = warp::serve(routes.clone()).run(([0, 0, 0, 0], ports.0)); + let cert_path = env::var("TLS_CERT_PATH").expect("TLS_CERT_PATH is not set"); + let key_path = env::var("TLS_CERT_KEY_PATH").expect("TLS_CERT_KEY_PATH is not set"); let https_handle = warp::serve(routes) .tls() - .cert_path("self.crt") - .key_path("self.key") - .run(([0, 0, 0, 0], get_ports().1)); + .cert_path(cert_path) + .key_path(key_path) + .run(([0, 0, 0, 0], ports.1)); (http_handle, https_handle, ws_handle) } @@ -306,13 +309,14 @@ async fn user_disconnected(my_id: usize, users: &Users) { } fn get_ports() -> (u16, u16) { - let http_port = env::var("RSPLAYER_HTTP_PORT") - .expect("RSPLAYER_HTTP_PORT is not set") + let http_port = env::var("PORT") + .expect("PORT is not set") .parse::() - .expect("RSPLAYER_HTTP_PORT is not a valid port number"); - let https_port = env::var("RSPLAYER_HTTPS_PORT") - .expect("RSPLAYER_HTTPS_PORT is not set") + .expect("PORT is not a valid port number"); + + let https_port = env::var("TLS_PORT") + .expect("TLS_PORT is not set") .parse::() - .expect("RSPLAYER_HTTPS_PORT is not a valid port number"); + .expect("TLS_PORT is not a valid port number"); (http_port, https_port) } diff --git a/rsplayer_hardware/src/audio_device/ak4497.rs b/rsplayer_hardware/src/audio_device/ak4497.rs index 14f6868..ec49cb6 100644 --- a/rsplayer_hardware/src/audio_device/ak4497.rs +++ b/rsplayer_hardware/src/audio_device/ak4497.rs @@ -98,12 +98,15 @@ impl DacAk4497 { // control 1 // ACKS = 1 (ignored when AFSD = 1) // AFSD = 1 - // PCM mode normal, mode 3, 16-bit I2S Compatible + // PCM mode normal, mode 3, 16-bit I2S Compatible (this mode is required for FifoPiMa output, otherwise it works in default mode which is tested with WaveIO) self.i2c_helper.write_register(0, 0b1000_0111); // control 2 - // DEM[1:0] = 00 - De-emphasis Filter Control on 44.1 kHz - // self.i2c_helper.write_register(1, 0b00100000); + // DEM[1:0] = 01 - De-emphasis Filter Control on 44.1 kHz + self.i2c_helper.write_register(1, 0b00000010); + // invert signal left and right channel + self.i2c_helper.change_bit(5, 7, true); + self.i2c_helper.change_bit(5, 6, true); self.set_vol(volume.current); self.filter(dac_settings.filter); self.set_gain(dac_settings.gain); diff --git a/rsplayer_playback/Cargo.toml b/rsplayer_playback/Cargo.toml index 0b51647..b54103d 100644 --- a/rsplayer_playback/Cargo.toml +++ b/rsplayer_playback/Cargo.toml @@ -16,6 +16,8 @@ env_logger.workspace = true anyhow.workspace = true symphonia.workspace = true tokio.workspace = true +thread-priority = "0.16.0" +core_affinity = "0.8.1" # symphonia cpal = "0.15.3" diff --git a/rsplayer_playback/src/rsp/mod.rs b/rsplayer_playback/src/rsp/mod.rs index dbb0fa4..0c79e39 100644 --- a/rsplayer_playback/src/rsp/mod.rs +++ b/rsplayer_playback/src/rsp/mod.rs @@ -2,13 +2,15 @@ use std::sync::{ atomic::{AtomicBool, AtomicU16, Ordering}, Arc, Mutex, }; +use std::thread::JoinHandle; use log::{error, info, warn}; +use thread_priority::{ThreadBuilder, ThreadPriority}; -use tokio::{sync::broadcast::Sender, task::JoinHandle}; +use tokio::sync::broadcast::Sender; use api_models::{ - settings::Settings, + settings::{RsPlayerSettings, Settings}, state::{PlayerState, StateChangeEvent}, }; use rsplayer_metadata::metadata_service::MetadataService; @@ -30,7 +32,7 @@ pub struct PlayerService { paused: Arc, skip_to_time: Arc, audio_device: String, - buffer_size_mb: usize, + rsp_settings: RsPlayerSettings, music_dir: String, } impl PlayerService { @@ -44,7 +46,7 @@ impl PlayerService { paused: Arc::new(AtomicBool::new(false)), skip_to_time: Arc::new(AtomicU16::new(0)), audio_device: settings.alsa_settings.output_device.name.clone(), - buffer_size_mb: settings.rs_player_settings.buffer_size_mb, + rsp_settings: settings.rs_player_settings.clone(), music_dir: settings.metadata_settings.music_directory.clone(), } } @@ -88,7 +90,7 @@ impl PlayerService { pub async fn stop_current_song(&self) { let this = self; this.running.store(false, Ordering::SeqCst); - self.await_playing_song_to_finish().await; + self.await_playing_song_to_finish(); } #[allow(clippy::unused_self, clippy::missing_const_for_fn)] @@ -103,12 +105,11 @@ impl PlayerService { } } - async fn await_playing_song_to_finish(&self) { + fn await_playing_song_to_finish(&self) { let Some(a) = self.play_handle.lock().unwrap().take() else { return; }; - a.abort(); - let _ = a.await; + _ = a.join(); } fn is_playing(&self) -> bool { @@ -126,65 +127,84 @@ impl PlayerService { let skip_to_time = self.skip_to_time.clone(); let queue = self.queue_service.clone(); let audio_device = self.audio_device.clone(); - let buffer_size = self.buffer_size_mb; + let playback_thread_prio = self.rsp_settings.player_threads_priority; let music_dir = self.music_dir.clone(); let queue_size = queue.get_all_songs().len(); let changes_tx = changes_tx.clone(); - tokio::task::spawn_blocking(move || { - let mut num_failed = 0; - loop { - let Some(song) = queue.get_current_song() else { - running.store(false, Ordering::SeqCst); - changes_tx - .send(StateChangeEvent::PlaybackStateEvent(PlayerState::STOPPED)) - .ok(); - break PlaybackResult::QueueFinished; - }; - changes_tx - .send(StateChangeEvent::CurrentSongEvent(song.clone())) - .expect("msg send failed"); - changes_tx - .send(StateChangeEvent::PlaybackStateEvent(PlayerState::PLAYING)) - .expect("msg send failed"); - match symphonia::play_file( - &song.file, - &running, - &paused, - &skip_to_time, - &audio_device, - buffer_size, - &music_dir, - &changes_tx, - ) { - Ok(PlaybackResult::PlaybackStopped) => { + let rsp_settings = self.rsp_settings.clone(); + + ThreadBuilder::default() + .name("playback".to_string()) + .priority(ThreadPriority::Crossplatform(playback_thread_prio.try_into().unwrap())) + .spawn(move |prio| { + if prio.is_ok() { + info!("Playback thread started with priority {:?}", playback_thread_prio); + } else { + warn!("Failed to set playback thread priority"); + } + + if let Some(Some(last_core)) = core_affinity::get_core_ids().map(|ids| ids.last().cloned()) { + if core_affinity::set_for_current(last_core) { + info!("Playback thread set to last core {:?}", last_core); + } else { + warn!("Failed to set playback thread to last core {:?}", last_core); + } + } + let mut num_failed = 0; + loop { + let Some(song) = queue.get_current_song() else { running.store(false, Ordering::SeqCst); changes_tx .send(StateChangeEvent::PlaybackStateEvent(PlayerState::STOPPED)) .ok(); - break PlaybackResult::PlaybackStopped; - } - Err(err) => { - error!("Failed to play file {}. Error: {:?}", song.file, err); - num_failed += 1; - if num_failed == 10 || num_failed >= queue_size { - warn!("Number of failed songs is greater than 10. Aborting."); + break PlaybackResult::QueueFinished; + }; + changes_tx + .send(StateChangeEvent::CurrentSongEvent(song.clone())) + .expect("msg send failed"); + changes_tx + .send(StateChangeEvent::PlaybackStateEvent(PlayerState::PLAYING)) + .expect("msg send failed"); + match symphonia::play_file( + &song.file, + &running, + &paused, + &skip_to_time, + &audio_device, + &rsp_settings, + &music_dir, + &changes_tx, + ) { + Ok(PlaybackResult::PlaybackStopped) => { running.store(false, Ordering::SeqCst); changes_tx .send(StateChangeEvent::PlaybackStateEvent(PlayerState::STOPPED)) .ok(); - break PlaybackResult::QueueFinished; + break PlaybackResult::PlaybackStopped; + } + Err(err) => { + error!("Failed to play file {}. Error: {:?}", song.file, err); + num_failed += 1; + if num_failed == 10 || num_failed >= queue_size { + warn!("Number of failed songs is greater than 10. Aborting."); + running.store(false, Ordering::SeqCst); + changes_tx + .send(StateChangeEvent::PlaybackStateEvent(PlayerState::STOPPED)) + .ok(); + break PlaybackResult::QueueFinished; + } + } + res => { + info!("Playback finished with result {:?}", res); + num_failed = 0; } } - res => { - info!("Playback finished with result {:?}", res); - num_failed = 0; - } - } - if !queue.move_current_to_next_song() { - break PlaybackResult::QueueFinished; + if !queue.move_current_to_next_song() { + break PlaybackResult::QueueFinished; + } } - } - }) + }) + .unwrap() } } diff --git a/rsplayer_playback/src/rsp/output.rs b/rsplayer_playback/src/rsp/output.rs index f5bba36..394d564 100644 --- a/rsplayer_playback/src/rsp/output.rs +++ b/rsplayer_playback/src/rsp/output.rs @@ -8,6 +8,8 @@ //! Platform-dependant Audio Outputs use anyhow::Result; +use api_models::settings::RsPlayerSettings; +use log::info; use symphonia::core::audio::{AudioBufferRef, SignalSpec}; pub trait AudioOutput { @@ -23,6 +25,7 @@ mod cpal { use super::AudioOutput; use anyhow::{Error, Result}; + use api_models::settings::RsPlayerSettings; use symphonia::core::audio::{AudioBufferRef, RawSample, SampleBuffer, SignalSpec}; use symphonia::core::conv::ConvertibleSample; @@ -42,10 +45,16 @@ mod cpal { impl AudioOutputSample for f32 {} impl AudioOutputSample for i16 {} impl AudioOutputSample for u16 {} + impl AudioOutputSample for u32 {} impl AudioOutputSample for i32 {} impl CpalAudioOutput { - pub fn try_open(spec: SignalSpec, duration: u64, audio_device: &str) -> Result> { + pub fn try_open( + spec: SignalSpec, + duration: u64, + audio_device: &str, + rsp_settings: &RsPlayerSettings, + ) -> Result> { // Get default host. let host = cpal::default_host(); let device = host @@ -53,6 +62,7 @@ mod cpal { .find(|d| d.name().unwrap_or_default() == audio_device) .ok_or_else(|| Error::msg(format!("Device {audio_device} not found!")))?; debug!("Spec: {:?}", spec); + let config = match device.default_output_config() { Ok(config) => config, Err(err) => { @@ -62,19 +72,23 @@ mod cpal { }; // Select proper playback routine based on sample format. - // CpalAudioOutputImpl::::try_open(spec, duration, &device); match config.sample_format() { - cpal::SampleFormat::F32 => CpalAudioOutputImpl::::try_open(spec, duration, &device), - cpal::SampleFormat::I32 => CpalAudioOutputImpl::::try_open(spec, duration, &device), - cpal::SampleFormat::I16 => CpalAudioOutputImpl::::try_open(spec, duration, &device), - cpal::SampleFormat::U16 => CpalAudioOutputImpl::::try_open(spec, duration, &device), - cpal::SampleFormat::I8 => todo!(), - cpal::SampleFormat::I64 => todo!(), - cpal::SampleFormat::U8 => todo!(), - cpal::SampleFormat::U32 => todo!(), - cpal::SampleFormat::U64 => todo!(), - cpal::SampleFormat::F64 => todo!(), - _ => todo!(), + cpal::SampleFormat::F32 => { + CpalAudioOutputImpl::::try_open(spec, duration, &device, rsp_settings) + } + cpal::SampleFormat::I32 => { + CpalAudioOutputImpl::::try_open(spec, duration, &device, rsp_settings) + } + cpal::SampleFormat::I16 => { + CpalAudioOutputImpl::::try_open(spec, duration, &device, rsp_settings) + } + cpal::SampleFormat::U16 => { + CpalAudioOutputImpl::::try_open(spec, duration, &device, rsp_settings) + } + cpal::SampleFormat::U32 => { + CpalAudioOutputImpl::::try_open(spec, duration, &device, rsp_settings) + } + _ => panic!("Unsupported sample format!"), } } } @@ -89,7 +103,12 @@ mod cpal { } impl CpalAudioOutputImpl { - pub fn try_open(spec: SignalSpec, duration: u64, device: &cpal::Device) -> Result> { + pub fn try_open( + spec: SignalSpec, + duration: u64, + device: &cpal::Device, + rsp_settings: &RsPlayerSettings, + ) -> Result> { let num_channels = spec.channels.count(); // Output audio stream config. @@ -97,11 +116,13 @@ mod cpal { let config = cpal::StreamConfig { channels: num_channels as cpal::ChannelCount, sample_rate: cpal::SampleRate(spec.rate), - buffer_size: cpal::BufferSize::Default, + buffer_size: rsp_settings + .alsa_buffer_size + .map_or(cpal::BufferSize::Default, cpal::BufferSize::Fixed), }; - // Create a ring buffer with a capacity for up-to 200ms of audio. - let ring_len = ((200 * spec.rate as usize) / 1000) * num_channels; + // Create a ring buffer with a capacity + let ring_len = ((rsp_settings.ring_buffer_size_ms * spec.rate as usize) / 1000) * num_channels; let ring_buf = SpscRb::new(ring_len); let (ring_buf_producer, ring_buf_consumer) = (ring_buf.producer(), ring_buf.consumer()); @@ -116,7 +137,7 @@ mod cpal { data[written..].iter_mut().for_each(|s| *s = T::MID); }, move |err| error!("audio output error: {}", err), - Some(Duration::from_secs(30)), + None, ); if let Err(err) = stream_result { @@ -177,10 +198,24 @@ mod cpal { } } -pub fn try_open(spec: SignalSpec, duration: u64, audio_device: &str) -> Result> { - let result = cpal::CpalAudioOutput::try_open(spec, duration, audio_device); +pub fn try_open( + spec: SignalSpec, + duration: u64, + audio_device: &str, + rsp_settings: &RsPlayerSettings, +) -> Result> { + let result = cpal::CpalAudioOutput::try_open(spec, duration, audio_device, rsp_settings); if result.is_err() && audio_device.starts_with("hw:") { - return cpal::CpalAudioOutput::try_open(spec, duration, &audio_device.replace("hw:", "plughw:")); + info!( + "Failed to open audio output {}. Trying with plughw: prefix.", + audio_device + ); + return cpal::CpalAudioOutput::try_open( + spec, + duration, + &audio_device.replace("hw:", "plughw:"), + rsp_settings, + ); } result } diff --git a/rsplayer_playback/src/rsp/symphonia.rs b/rsplayer_playback/src/rsp/symphonia.rs index 1bc9841..514d665 100644 --- a/rsplayer_playback/src/rsp/symphonia.rs +++ b/rsplayer_playback/src/rsp/symphonia.rs @@ -6,13 +6,12 @@ use std::thread::{self}; use std::time::Duration; use anyhow::{format_err, Result}; - -use api_models::state::{PlayerInfo, SongProgress, StateChangeEvent}; +use api_models::settings::RsPlayerSettings; use log::{debug, info, warn}; use symphonia::core::audio::Channels; use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL}; use symphonia::core::errors::Error; -use symphonia::core::formats::{FormatOptions, FormatReader, Track}; +use symphonia::core::formats::{FormatOptions, FormatReader, SeekMode, SeekTo, Track}; use symphonia::core::io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions, ReadOnlySource}; use symphonia::core::meta::MetadataOptions; use symphonia::core::probe::Hint; @@ -20,6 +19,8 @@ use symphonia::core::units::{Time, TimeBase}; use symphonia::default::{get_codecs, get_probe}; use tokio::sync::broadcast::Sender; +use api_models::state::{PlayerInfo, SongProgress, StateChangeEvent}; + use crate::rsp::output::AudioOutput; use super::output::try_open; @@ -30,7 +31,9 @@ pub enum PlaybackResult { SongFinished, PlaybackStopped, } + unsafe impl Send for PlaybackResult {} + #[allow(clippy::type_complexity, clippy::too_many_arguments, clippy::too_many_lines)] pub fn play_file( path_str: &str, @@ -38,7 +41,7 @@ pub fn play_file( paused: &Arc, skip_to_time: &Arc, audio_device: &str, - buffer_size_mb: usize, + rsp_settings: &RsPlayerSettings, music_dir: &str, changes_tx: &Sender, ) -> Result { @@ -52,7 +55,7 @@ pub fn play_file( MediaSourceStream::new( source, MediaSourceStreamOptions { - buffer_len: (buffer_size_mb * 1024 * 1024).next_power_of_two(), + buffer_len: (rsp_settings.input_stream_buffer_size_mb * 1024 * 1024).next_power_of_two(), }, ), &FormatOptions::default(), @@ -78,7 +81,7 @@ pub fn play_file( let rate = codec_parameters.sample_rate; let bps = codec_parameters.bits_per_sample; let chan_num = codec_parameters.channels.map(Channels::count); - let cd = symphonia::default::get_codecs().get_codec(codec_parameters.codec); + let cd = get_codecs().get_codec(codec_parameters.codec); changes_tx .send(StateChangeEvent::PlayerInfoEvent(PlayerInfo { audio_format_bit: bps, @@ -99,7 +102,8 @@ pub fn play_file( debug!("Exit from play thread due to running flag change"); break Ok(PlaybackResult::PlaybackStopped); } - if paused.load(Ordering::SeqCst) { + let paused = paused.load(Ordering::SeqCst); + if paused { debug!("Playing paused, going to sleep"); thread::sleep(Duration::from_millis(300)); paused_time += 300; @@ -113,8 +117,8 @@ pub fn play_file( let skip_to = skip_to_time.swap(0, Ordering::SeqCst); debug!("Seeking to {}", skip_to); let seek_result = reader.seek( - symphonia::core::formats::SeekMode::Accurate, - symphonia::core::formats::SeekTo::Time { + SeekMode::Accurate, + SeekTo::Time { time: Time::new(u64::from(skip_to), 0.0), track_id: Some(track_id), }, @@ -158,7 +162,7 @@ pub fn play_file( let duration = decoded_buff.capacity() as u64; // Try to open the audio output. - let Ok(audio_out) = try_open(spec, duration, audio_device) else { + let Ok(audio_out) = try_open(spec, duration, audio_device, rsp_settings) else { break Err(format_err!("Failed to open audio output {}", audio_device)); }; debug!("Audio opened"); diff --git a/rsplayer_web_ui/src/lib.rs b/rsplayer_web_ui/src/lib.rs index 137f268..2cfacb2 100644 --- a/rsplayer_web_ui/src/lib.rs +++ b/rsplayer_web_ui/src/lib.rs @@ -644,7 +644,13 @@ fn view_player_footer(page: &Page, player_model: &PlayerModel) -> Node { C!["level", "is-mobile"], // image div![ - C!["level-left", "is-flex-grow-1", "is-hidden-mobile", "is-clickable", "mr-2"], + C![ + "level-left", + "is-flex-grow-1", + "is-hidden-mobile", + "is-clickable", + "mr-2" + ], div![ C!["level-item"], figure![ @@ -660,7 +666,7 @@ fn view_player_footer(page: &Page, player_model: &PlayerModel) -> Node { div![ C!["level-left", "is-flex-grow-3", "is-clickable"], div![ - C!["level-item","is-justify-content-flex-start", "available-width"], + C!["level-item", "is-justify-content-flex-start", "available-width"], div![ p![ C!["heading", "has-overflow-ellipsis-text"], diff --git a/rsplayer_web_ui/src/page/settings.rs b/rsplayer_web_ui/src/page/settings.rs index ebf0993..0d30a3e 100644 --- a/rsplayer_web_ui/src/page/settings.rs +++ b/rsplayer_web_ui/src/page/settings.rs @@ -8,10 +8,11 @@ use api_models::{ DacSettings, IRInputControlerSettings, MetadataStoreSettings, OLEDSettings, OutputSelectorSettings, RsPlayerSettings, Settings, }, + validator::Validate, }; use gloo_console::log; use gloo_net::{http::Request, Error}; -use seed::{attrs, button, div, h1, input, label, option, p, prelude::*, section, select, C, IF}; +use seed::{attrs, button, div, h1, i, input, label, option, prelude::*, section, select, span, style, C, IF}; use strum::IntoEnumIterator; use crate::view_spinner_modal; @@ -38,6 +39,7 @@ pub enum Msg { ToggleOutputSelectorEnabled, ToggleRotaryVolume, ToggleResumePlayback, + ToggleRspAlsaBufferSize, // ---- Input capture ---- InputMetadataMusicDirectoryChanged(String), InputAlsaCardChange(i32), @@ -47,7 +49,10 @@ pub enum Msg { InputRotaryEventDevicePathChanged(String), InputVolumeStepChanged(String), InputVolumeCtrlDeviceChanged(VolumeCrtlType), - InputRspBufferSizeChange(String), + InputRspInputBufferSizeChange(String), + InputRspAudioBufferSizeChange(String), + InputRspAlsaBufferSizeChange(String), + InputRspThreadPriorityChange(String), InputVolumeAlsaMixerChanged(String), InputDacAddressChanged(String), ClickRescanMetadataButton, @@ -99,10 +104,13 @@ pub fn init(_url: Url, orders: &mut impl Orders) -> Model { pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { match msg { Msg::SaveSettingsAndRestart => { - // todo: show modal wait window while server is restarting. use ws status. - let settings = model.settings.clone(); - orders.perform_cmd(async { Msg::SettingsSaved(save_settings(settings, "reload=true".to_string()).await) }); - model.waiting_response = true; + if model.settings.validate().is_ok() { + let settings = model.settings.clone(); + orders.perform_cmd(async { + Msg::SettingsSaved(save_settings(settings, "reload=true".to_string()).await) + }); + model.waiting_response = true; + } } Msg::ToggleDacEnabled => { model.settings.dac_settings.enabled = !model.settings.dac_settings.enabled; @@ -122,6 +130,13 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { Msg::ToggleResumePlayback => { model.settings.auto_resume_playback = !model.settings.auto_resume_playback; } + Msg::ToggleRspAlsaBufferSize => { + if model.settings.rs_player_settings.alsa_buffer_size.is_some() { + model.settings.rs_player_settings.alsa_buffer_size = None; + } else { + model.settings.rs_player_settings.alsa_buffer_size = Some(10000); + } + } Msg::InputMetadataMusicDirectoryChanged(value) => { model.settings.metadata_settings.music_directory = value; @@ -166,9 +181,26 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { Msg::InputRotaryEventDevicePathChanged(path) => { model.settings.volume_ctrl_settings.rotary_event_device_path = path; } - Msg::InputRspBufferSizeChange(value) => { + Msg::InputRspInputBufferSizeChange(value) => { + if let Ok(num) = value.parse::() { + model.settings.rs_player_settings.input_stream_buffer_size_mb = num; + }; + } + Msg::InputRspAudioBufferSizeChange(value) => { if let Ok(num) = value.parse::() { - model.settings.rs_player_settings.buffer_size_mb = num; + model.settings.rs_player_settings.ring_buffer_size_ms = num; + }; + } + Msg::InputRspAlsaBufferSizeChange(value) => { + if let Ok(num) = value.parse::() { + model.settings.rs_player_settings.alsa_buffer_size = Some(num); + }; + } + Msg::InputRspThreadPriorityChange(value) => { + if let Ok(num) = value.parse::() { + if num > 0 && num < 100 { + model.settings.rs_player_settings.player_threads_priority = num; + } }; } Msg::InputDacAddressChanged(value) => { @@ -210,26 +242,6 @@ pub fn view(model: &Model) -> Node { section![ C!["section"], h1![C!["title","has-text-white"], "General"], - view_rsp(&settings.rs_player_settings), - div![ - C!["field"], - ev(Ev::Click, |_| Msg::ToggleResumePlayback), - input![ - C!["switch"], - attrs! { - At::Name => "resume_playback_cb" - At::Type => "checkbox" - At::Checked => settings.auto_resume_playback.as_at_value(), - }, - ], - label![ - C!["label","has-text-white"], - "Auto resume playback on start", - attrs! { - At::For => "resume_playback_cb" - } - ] - ], div![ C!["field", "is-grouped","is-grouped-multiline"], div![C!["control"], @@ -254,7 +266,6 @@ pub fn view(model: &Model) -> Node { }), ], ], - p![C!["control"],"->"], div![C!["control"], label!["PCM Device", C!["label","has-text-white"]], div![ @@ -274,9 +285,28 @@ pub fn view(model: &Model) -> Node { input_ev(Ev::Change, Msg::InputAlsaPcmChange), ], ] - ], + view_rsp(&settings.rs_player_settings), view_metadata_storage(&model.settings.metadata_settings), + div![ + C!["field", "mt-5"], + ev(Ev::Click, |_| Msg::ToggleResumePlayback), + input![ + C!["switch"], + attrs! { + At::Name => "resume_playback_cb" + At::Type => "checkbox" + At::Checked => settings.auto_resume_playback.as_at_value(), + }, + ], + label![ + C!["label","has-text-white"], + "Auto resume playback on start", + attrs! { + At::For => "resume_playback_cb" + } + ] + ], ], // volume control section![ @@ -386,32 +416,46 @@ pub fn view(model: &Model) -> Node { div![ C!["buttons"], button![ - C!["button", "is-dark"], + IF!(model.settings.validate().is_err() => attrs!{ At::Disabled => ""}), + C!["button", "is-primary"], "Save & restart player", ev(Ev::Click, |_| Msg::SaveSettingsAndRestart) ], button![ - C!["button", "is-dark"], + C!["button", "is-primary"], "Restart player", ev(Ev::Click, |_| Msg::SendSystemCommand( SystemCommand::RestartRSPlayer )) ], button![ - C!["button", "is-dark"], + C!["button", "is-primary"], "Restart system", ev(Ev::Click, |_| Msg::SendSystemCommand( SystemCommand::RestartSystem )) ], button![ - C!["button", "is-dark"], + C!["button", "is-primary"], "Shutdown system", ev(Ev::Click, |_| Msg::SendSystemCommand(SystemCommand::PowerOff)) ] ] ] } +fn view_validation_icon(val: &impl Validate, key: &str) -> Node { + let class = if let Err(errors) = val.validate() { + if errors.errors().contains_key(key) { + "fa-exclamation-triangle" + } else { + "fa-check" + } + } else { + "fa-check" + }; + + span![C!["icon", "is-small", "is-right"], i![C!["fas", class]]] +} // ------ sub view functions ------ fn view_ir_control(ir_settings: &IRInputControlerSettings) -> Node { @@ -602,10 +646,7 @@ fn view_dac(dac_settings: &DacSettings) -> Node { label!["DAC I2C address:", C!["label", "has-text-white"]], div![ C!["control"], - input![ - C!["input"], - attrs! {At::Value => dac_settings.i2c_address}, - ], + input![C!["input"], attrs! {At::Value => dac_settings.i2c_address},], input_ev(Ev::Input, move |value| { Msg::InputDacAddressChanged(value) }), ], ], @@ -730,15 +771,74 @@ fn view_metadata_storage(metadata_settings: &MetadataStoreSettings) -> Node fn view_rsp(rsp_settings: &RsPlayerSettings) -> Node { div![ C!["field"], - label!["Input buffer size (in MB)", C!["label", "has-text-white"]], + label!["Input buffer size (MB) (1-200)", C!["label", "has-text-white", "mt-5"]], div![ - C!["control"], + C!["control", "has-icons-right"], + style! {St::Width => "max-content"}, input![ C!["input"], - attrs! {At::Value => rsp_settings.buffer_size_mb, At::Type => "number"}, - input_ev(Ev::Input, move |value| { Msg::InputRspBufferSizeChange(value) }), + attrs! {At::Value => rsp_settings.input_stream_buffer_size_mb, At::Type => "number"}, + input_ev(Ev::Input, move |value| { Msg::InputRspInputBufferSizeChange(value) }), ], + view_validation_icon(rsp_settings, "input_stream_buffer_size_mb") ], + label!["Ring buffer size (1-10000ms)", C!["label", "has-text-white", "mt-5"]], + div![ + C!["control", "has-icons-right"], + style! {St::Width => "max-content"}, + input![ + C!["input"], + attrs! {At::Value => rsp_settings.ring_buffer_size_ms, At::Type => "number"}, + input_ev(Ev::Input, move |value| { Msg::InputRspAudioBufferSizeChange(value) }), + ], + view_validation_icon(rsp_settings, "ring_buffer_size_ms") + ], + label!["Player thread priority (1-99)", C!["label", "has-text-white", "mt-5"]], + div![ + C!["control", "has-icons-right"], + style! {St::Width => "max-content"}, + input![ + C!["input"], + attrs! {At::Value => rsp_settings.player_threads_priority, At::Type => "number"}, + input_ev(Ev::Input, move |value| { Msg::InputRspThreadPriorityChange(value) }), + ], + view_validation_icon(rsp_settings, "player_threads_priority") + ], + div![ + C!["field", "mt-5"], + ev(Ev::Click, |_| Msg::ToggleRspAlsaBufferSize), + input![ + C!["switch"], + attrs! { + At::Name => "alsabufsize_cb" + At::Type => "checkbox" + At::Checked => rsp_settings.alsa_buffer_size.is_some().as_at_value(), + }, + ], + label![ + C!["label", "has-text-white"], + "Set alsa buffer frame size (Experimental!)", + attrs! { + At::For => "alsabufsize_cb" + } + ] + ], + IF!(rsp_settings.alsa_buffer_size.is_some() => + div![ + C!["field"], + div![ + C!["control"], + input![ + C!["input"], + attrs! { + At::Value => rsp_settings.alsa_buffer_size.unwrap_or(10000), + At::Type => "number" + }, + input_ev(Ev::Input, move |value| { Msg::InputRspAlsaBufferSizeChange(value) }), + ], + ], + ] + ) ] }