Rockbox open source high quality audio player as a Music Player Daemon
mpris rockbox mpd libadwaita audio rust zig deno
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Expose current volume and wire UI

Add GET /player/volume handler and VolumeInfo GraphQL query.
Implement Sound RPC's sound_current to return the actual setting.
Have GPUI fetch live volume on startup and map dB→0–100 for the slider.
Update min volume constant to -80 and adapt web UI slider mapping.

+136 -34
+1 -1
crates/graphql/src/schema/objects/user_settings.rs
··· 1061 1061 fn from(settings: rb::types::user_settings::UserSettings) -> Self { 1062 1062 Self { 1063 1063 music_dir: settings.music_dir, 1064 - volume: 0, 1064 + volume: rb::sound::current(0), 1065 1065 balance: settings.balance, 1066 1066 bass: settings.bass, 1067 1067 treble: settings.treble,
+21 -6
crates/graphql/src/schema/sound.rs
··· 2 2 3 3 use crate::rockbox_url; 4 4 5 + #[derive(SimpleObject)] 6 + struct VolumeInfo { 7 + volume: i32, 8 + min: i32, 9 + max: i32, 10 + } 11 + 5 12 #[derive(Default)] 6 13 pub struct SoundQuery; 7 14 8 15 #[Object] 9 16 impl SoundQuery { 10 - async fn sound_current(&self) -> String { 11 - "sound".to_string() 17 + async fn volume(&self, ctx: &Context<'_>) -> Result<VolumeInfo, Error> { 18 + let client = ctx.data::<reqwest::Client>().unwrap(); 19 + let url = format!("{}/player/volume", rockbox_url()); 20 + let resp = client.get(&url).send().await?; 21 + let body: serde_json::Value = resp.json().await?; 22 + Ok(VolumeInfo { 23 + volume: body["volume"].as_i64().unwrap_or(0) as i32, 24 + min: body["min"].as_i64().unwrap_or(-80) as i32, 25 + max: body["max"].as_i64().unwrap_or(0) as i32, 26 + }) 12 27 } 13 28 14 29 async fn sound_default(&self) -> String { ··· 31 46 impl SoundMutation { 32 47 async fn adjust_volume(&self, ctx: &Context<'_>, steps: i32) -> Result<i32, Error> { 33 48 let client = ctx.data::<reqwest::Client>().unwrap(); 34 - let body = serde_json::json!({ 35 - "steps": steps, 36 - }); 49 + let body = serde_json::json!({ "steps": steps }); 37 50 let url = format!("{}/player/volume", rockbox_url()); 38 51 client.put(&url).json(&body).send().await?; 39 52 40 - Ok(0) 53 + let resp = client.get(&url).send().await?; 54 + let info: serde_json::Value = resp.json().await?; 55 + Ok(info["volume"].as_i64().unwrap_or(0) as i32) 41 56 } 42 57 43 58 async fn sound_set(&self) -> String {
+4 -2
crates/rpc/src/sound.rs
··· 44 44 45 45 async fn sound_current( 46 46 &self, 47 - _request: tonic::Request<SoundCurrentRequest>, 47 + request: tonic::Request<SoundCurrentRequest>, 48 48 ) -> Result<tonic::Response<SoundCurrentResponse>, tonic::Status> { 49 - Ok(tonic::Response::new(SoundCurrentResponse::default())) 49 + let setting = request.into_inner().setting; 50 + let value = rockbox_sys::sound::current(setting); 51 + Ok(tonic::Response::new(SoundCurrentResponse { value })) 50 52 } 51 53 52 54 async fn sound_default(
+1
crates/server/src/handlers/mod.rs
··· 50 50 async_handler!(player, previous); 51 51 async_handler!(player, stop); 52 52 async_handler!(player, get_file_position); 53 + async_handler!(player, get_volume); 53 54 async_handler!(player, adjust_volume); 54 55 async_handler!(player, get_current_player); 55 56 async_handler!(playlists, create_playlist);
+9
crates/server/src/handlers/player.rs
··· 455 455 Ok(()) 456 456 } 457 457 458 + pub async fn get_volume(_ctx: &Context, _req: &Request, res: &mut Response) -> Result<(), Error> { 459 + const SOUND_VOLUME: i32 = 0; 460 + let volume = rb::sound::current(SOUND_VOLUME); 461 + let min = rb::sound::min(SOUND_VOLUME); 462 + let max = rb::sound::max(SOUND_VOLUME); 463 + res.json(&serde_json::json!({ "volume": volume, "min": min, "max": max })); 464 + Ok(()) 465 + } 466 + 458 467 pub async fn adjust_volume(_ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { 459 468 let req_body = req.body.as_ref().unwrap(); 460 469 let new_volume: NewVolume = serde_json::from_str(&req_body).unwrap();
+1
crates/server/src/lib.rs
··· 106 106 app.put("/player/previous", previous); 107 107 app.put("/player/stop", stop); 108 108 app.get("/player/file-position", get_file_position); 109 + app.get("/player/volume", get_volume); 109 110 app.put("/player/volume", adjust_volume); 110 111 111 112 app.post("/playlists", create_playlist);
+18 -4
gpui/src/client.rs
··· 9 9 LikeTrackRequest, NextRequest, PauseRequest, PlayAlbumRequest, PlayAllTracksRequest, 10 10 PlayArtistTracksRequest, PlayDirectoryRequest, PlayTrackRequest, PlaylistResumeRequest, 11 11 PreviousRequest, RemoveTracksRequest, ResumeRequest, ResumeTrackRequest, SaveSettingsRequest, 12 - SearchRequest, ShufflePlaylistRequest, StartRequest, StatusRequest, StreamCurrentTrackRequest, 13 - StreamLibraryRequest, StreamPlaylistRequest, StreamStatusRequest, TreeGetEntriesRequest, 14 - UnlikeTrackRequest, 12 + SearchRequest, ShufflePlaylistRequest, SoundCurrentRequest, StartRequest, StatusRequest, 13 + StreamCurrentTrackRequest, StreamLibraryRequest, StreamPlaylistRequest, StreamStatusRequest, 14 + TreeGetEntriesRequest, UnlikeTrackRequest, 15 15 }; 16 16 use crate::state::{DeviceItem, SearchAlbum, SearchArtist, SearchPlaylist, SearchResults}; 17 17 ··· 333 333 Ok(()) 334 334 } 335 335 336 + pub async fn get_current_volume() -> Result<i32> { 337 + const SOUND_VOLUME: i32 = 0; 338 + let mut c = SoundServiceClient::connect(URL).await?; 339 + let resp = c 340 + .sound_current(SoundCurrentRequest { 341 + setting: SOUND_VOLUME, 342 + }) 343 + .await?; 344 + Ok(resp.into_inner().value) 345 + } 346 + 336 347 // ── Settings (shuffle, repeat) ──────────────────────────────────────────────── 337 348 338 349 pub async fn save_shuffle(enabled: bool) -> Result<()> { ··· 395 406 } 396 407 397 408 pub async fn run_settings_sync(tx: Sender<StateUpdate>) { 409 + let live_volume = get_current_volume().await.ok(); 410 + 398 411 match SettingsServiceClient::connect(URL).await { 399 412 Ok(mut c) => match c.get_global_settings(GetGlobalSettingsRequest {}).await { 400 413 Ok(resp) => { 401 414 let s = resp.into_inner(); 415 + let volume = live_volume.unwrap_or(s.volume); 402 416 let _ = tx 403 417 .send(StateUpdate::Settings { 404 - volume: s.volume, 418 + volume, 405 419 shuffling: s.playlist_shuffle, 406 420 repeat_mode: s.repeat_mode, 407 421 })
+2 -2
gpui/src/state.rs
··· 80 80 Paused, 81 81 } 82 82 83 - // Rockbox volume is in dB; typical SDL target range -74..0 84 - pub const VOLUME_MIN_DB: i32 = -74; 83 + // Rockbox volume is in dB; SDL target range is -80..0 (AUDIOHW_SETTING in sdl_codec.h) 84 + pub const VOLUME_MIN_DB: i32 = -80; 85 85 pub const VOLUME_MAX_DB: i32 = 0; 86 86 87 87 pub fn volume_fraction(db: i32) -> f32 {
+29 -12
gpui/src/ui/components/miniplayer.rs
··· 11 11 use crate::ui::theme::Theme; 12 12 use gpui::prelude::FluentBuilder; 13 13 use gpui::{ 14 - div, img, px, relative, App, Context, FontWeight, InteractiveElement, IntoElement, 14 + div, img, px, App, Context, FontWeight, InteractiveElement, IntoElement, 15 15 ObjectFit, ParentElement, Render, ScrollWheelEvent, StatefulInteractiveElement, Styled, 16 16 StyledImage, Window, 17 17 }; ··· 415 415 .child( 416 416 div() 417 417 .w_24() 418 - .h(px(4.0)) 419 - .rounded_full() 420 - .cursor_pointer() 421 - .bg(theme.volume_slider_track) 422 418 .on_scroll_wheel( 423 419 |event: &ScrollWheelEvent, _window, cx: &mut App| { 424 420 let delta = event.delta.pixel_delta(px(12.0)); ··· 441 437 } 442 438 }, 443 439 ) 444 - .child( 445 - div() 446 - .h_full() 447 - .rounded_full() 448 - .bg(theme.volume_slider_fill) 449 - .w(relative(vol_fill)), 450 - ), 440 + .child({ 441 + let state_ref = cx.global::<Controller>().state.clone(); 442 + let rt = cx.global::<Controller>().rt(); 443 + SeekBar::new( 444 + "vol_bar", 445 + vol_fill, 446 + theme.volume_slider_track, 447 + theme.volume_slider_fill, 448 + px(4.0), 449 + ) 450 + .on_seek(move |fraction, _window, cx| { 451 + let range = (VOLUME_MAX_DB - VOLUME_MIN_DB) as f32; 452 + let new_vol = (VOLUME_MIN_DB as f32 453 + + fraction * range) 454 + .round() as i32; 455 + let steps = { 456 + let current = state_ref.read(cx).volume; 457 + new_vol - current 458 + }; 459 + if steps != 0 { 460 + state_ref.update(cx, |s, cx| { 461 + s.volume = new_vol; 462 + cx.notify(); 463 + }); 464 + rt.spawn(adjust_volume(steps)); 465 + } 466 + }) 467 + }), 451 468 ) 452 469 .child( 453 470 div()
+2
webui/rockbox/src/Components/ControlBar/RightMenu/Volume/Volume.tsx
··· 29 29 <Slider 30 30 aria-label="Volume" 31 31 value={volume} 32 + min={0} 33 + max={100} 32 34 onChange={(_event, value) => setVolume(value as number)} 33 35 onChangeCommitted={handleVolumeChange} 34 36 sx={{
+18 -7
webui/rockbox/src/Components/ControlBar/RightMenu/Volume/VolumeWithData.tsx
··· 1 1 import { 2 2 useAdjustVolumeMutation, 3 - useGetGlobalSettingsQuery, 3 + useGetVolumeQuery, 4 4 } from "../../../../Hooks/GraphQL"; 5 5 import Volume from "./Volume"; 6 6 import { FC, useMemo } from "react"; 7 7 8 8 const VolumeWithData: FC = () => { 9 - const { data, refetch } = useGetGlobalSettingsQuery(); 9 + const { data, refetch } = useGetVolumeQuery(); 10 10 const { mutateAsync: adjustVolumeAsync } = useAdjustVolumeMutation(); 11 + 12 + const min = data?.volume.min ?? -80; 13 + const max = data?.volume.max ?? 0; 14 + const currentDb = data?.volume.volume ?? min; 15 + 16 + // Map dB value to 0–100 range for the slider 17 + const range = max - min; 11 18 const volume = useMemo(() => { 12 - return Math.min((data?.globalSettings.volume || 0) + 80, 80); 13 - }, [data]); 19 + return range > 0 ? Math.round(((currentDb - min) / range) * 100) : 0; 20 + }, [currentDb, min, max]); 14 21 15 22 const onVolumeChange = async (newVolume: number) => { 16 - const steps = Math.min(newVolume, 80) - Math.min(volume, 80); 17 - await adjustVolumeAsync({ steps }); 18 - await refetch(); 23 + // newVolume is 0–100; convert to dB then compute steps 24 + const newDb = range > 0 ? Math.round((newVolume / 100) * range + min) : min; 25 + const steps = newDb - currentDb; 26 + if (steps !== 0) { 27 + await adjustVolumeAsync({ steps }); 28 + await refetch(); 29 + } 19 30 }; 20 31 21 32 return <Volume volume={volume} onVolumeChange={onVolumeChange} />;
+30
webui/rockbox/src/Hooks/GraphQL.tsx
··· 3038 3038 PlaybackStatusDocument.toString() 3039 3039 ); 3040 3040 3041 + export type GetVolumeQuery = { __typename?: 'Query', volume: { __typename?: 'VolumeInfo', volume: number, min: number, max: number } }; 3042 + 3043 + export const GetVolumeDocument = new TypedDocumentString(` 3044 + query GetVolume { 3045 + volume { 3046 + volume 3047 + min 3048 + max 3049 + } 3050 + } 3051 + `); 3052 + 3053 + export const useGetVolumeQuery = < 3054 + TData = GetVolumeQuery, 3055 + TError = unknown 3056 + >( 3057 + variables?: Record<string, never>, 3058 + options?: Omit<UseQueryOptions<GetVolumeQuery, TError, TData>, 'queryKey'> & { queryKey?: UseQueryOptions<GetVolumeQuery, TError, TData>['queryKey'] } 3059 + ) => { 3060 + return useQuery<GetVolumeQuery, TError, TData>( 3061 + { 3062 + queryKey: ['GetVolume'], 3063 + queryFn: fetchData<GetVolumeQuery, Record<string, never>>(GetVolumeDocument, variables), 3064 + ...options 3065 + } 3066 + )}; 3067 + 3068 + useGetVolumeQuery.document = GetVolumeDocument; 3069 + useGetVolumeQuery.getKey = () => ['GetVolume']; 3070 + 3041 3071 export const usePlaylistChangedSubscription = () => 3042 3072 useSubscription<PlaylistChangedSubscription>( 3043 3073 PlaylistChangedDocument.toString()