Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lang/template.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
" Join us on Discord ": "",
" OK ": "",
" OK ": "",
"\"Fit each screen\" keeps the layout you see on a 1920×1080 screen, so moving the window to a laptop or a 1440p or 4K screen shows the same amount of everything. \"Same size in pixels\" ignores the display scaling set in Windows.": "",
"(chosen files are used instead of the folder)": "",
"(deleted rule)": "",
"(not connected)": "",
Expand Down Expand Up @@ -102,6 +103,7 @@
"Browse": "",
"COMING UP NEXT": "",
"Can't be read (damaged or not audio)": "",
"Can't show the waveform": "",
"Cancel": "",
"Cart wall": "",
"Categories": "",
Expand Down Expand Up @@ -224,11 +226,13 @@
"Find stations on this server": "",
"Find this song in the local library and add it to the playlist": "",
"First run: create your administrator account.": "",
"Fit each screen like 1920×1080": "",
"Fixed crossfade": "",
"Fixed keys": "",
"Flat": "",
"Folder…": "",
"Follow": "",
"Follow each screen's display scaling": "",
"Forget the hand-set cues for this track": "",
"Format": "",
"Forward": "",
Expand Down Expand Up @@ -403,6 +407,7 @@
"On": "",
"On / off": "",
"On air": "",
"On screens with other display scaling or resolutions": "",
"Only": "",
"Only administrators can manage user accounts.": "",
"Only allow during scheduled times": "",
Expand Down Expand Up @@ -532,6 +537,7 @@
"STUDIO CLOCK": "",
"Same artist and title, and lengths within 3 seconds. Off: only byte-for-byte identical copies.": "",
"Same as time announcements": "",
"Same size in pixels on every screen": "",
"Save": "",
"Save and connect": "",
"Save configuration": "",
Expand Down Expand Up @@ -737,12 +743,14 @@
"all day": "",
"all files in order": "",
"all of them": "",
"analysing next track…": "",
"analysing…": "",
"any": "",
"any date": "",
"at": "",
"audio file": "",
"below": "",
"can't analyse the next track": "",
"command, e.g. PLAYLIST Evening": "",
"cut tail after": "",
"delete for good": "",
Expand Down
18 changes: 13 additions & 5 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1286,7 +1286,7 @@ impl App {
ui.horizontal(|ui| {
ui.label(core.notify.lock().clone());
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(Local::now().format("%a %d %b %H:%M:%S").to_string());
ui.label(core.now().format("%a %d %b %H:%M:%S").to_string());
ui.separator();
let live = core.handles.iter().flatten().filter(|h| h.status.lock().state == EncState::Live).count();
ui.label(RichText::new(format!("● {live} stream(s) live")).color(if live > 0 { GREEN } else { GRAY }));
Expand All @@ -1303,7 +1303,7 @@ impl App {
fn player(&mut self, ctx: &egui::Context, core: &mut Playout) {
egui::TopBottomPanel::top("player").show(ctx, |ui| {
ui.add_space(4.0);
let now = Local::now();
let now = core.now();
let st = core.status.clone();
let title = Some(core.on_air_text()).filter(|t| !t.is_empty()).unwrap_or_else(|| "No track playing".into());
let next_i = core.current.map(|i| i + 1).unwrap_or(0);
Expand Down Expand Up @@ -1645,8 +1645,9 @@ impl App {
ui.vertical_centered(|ui| {
ui.add_space(12.0);
ui.label(RichText::new(tr("STUDIO CLOCK")).small().color(GRAY));
ui.label(RichText::new(Local::now().format("%H:%M:%S").to_string()).monospace().size(51.0).strong().color(ACCENT));
ui.label(Local::now().format("%A, %d %B %Y").to_string());
let now = core.now();
ui.label(RichText::new(now.format("%H:%M:%S").to_string()).monospace().size(51.0).strong().color(ACCENT));
ui.label(now.format("%A, %d %B %Y").to_string());
if let Some(m) = core.secs_to_next_marker() {
ui.label(
RichText::new(format!("Next hour marker in {:02}:{:02}:{:02}", m / 3600, (m / 60) % 60, m % 60))
Expand Down Expand Up @@ -1818,6 +1819,12 @@ impl App {
}
});
ui.small(tr("Make everything larger or smaller if the text is hard to read."));
ui.add_space(4.0);
ui.label(tr("On screens with other display scaling or resolutions"));
for m in crate::theme::ScaleMode::ALL {
ui.radio_value(&mut self.workzones.scale_mode, m, tr(m.label()));
}
ui.small(tr("\"Fit each screen\" keeps the layout you see on a 1920×1080 screen, so moving the window to a laptop or a 1440p or 4K screen shows the same amount of everything. \"Same size in pixels\" ignores the display scaling set in Windows."));
ui.separator();
ui.horizontal(|ui| {
ui.label(tr("Work zone"));
Expand Down Expand Up @@ -2409,7 +2416,7 @@ impl App {
let mut del = None;
let mut run_now = None;
let opts = ActionOpts::of(core);
let now = Local::now();
let now = core.now();
egui::ScrollArea::vertical().show(ui, |ui| {
if core.cfg.events.is_empty() {
ui.label(RichText::new(tr("No events yet.")).color(GRAY));
Expand Down Expand Up @@ -3394,6 +3401,7 @@ impl eframe::App for App {
}

fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
crate::theme::fit_zoom(ctx, self.workzones.scale_mode);
// redraw ~10×/s while audio is moving (VU meters, elapsed time), ~3×/s when idle (the clock)
// (once a second while minimized with no Additional windows: nothing is on screen, and playback
// runs on its own threads)
Expand Down
77 changes: 74 additions & 3 deletions src/link.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! The desktop app's side of RadioMaster Server: signs in, keeps the mirror playout in step with the
//! server (about four times a second) and sends it the actions taken in the app.

use crate::playout::sync::{self, digest, Cmd, Snapshot, TabsWire};
use crate::playout::sync::{self, digest, Cmd, Snapshot, TabsWire, WaveReply};
use crate::playout::Playout;
use crossbeam_channel::{Receiver, RecvTimeoutError};
use parking_lot::Mutex;
Expand Down Expand Up @@ -263,11 +263,20 @@ impl Link {
log_rev: 0,
hist_rev: 0,
lib_ver: 0,
miss_rev: 0,
clock: None,
tabs_hash: None,
cfg_hash: None,
notify: String::new(),
queued: vec![],
};
// the track editor's waveforms come from the server, which has the files
let (wtx, wrx) = crossbeam_channel::unbounded();
s.core.lock().set_wave_source(wtx);
Comment thread
epell-development marked this conversation as resolved.
Comment thread
epell-development marked this conversation as resolved.
Comment thread
epell-development marked this conversation as resolved.
{
let (core, base, pin, token, stop) = (s.core.clone(), session.base.clone(), session.pin.clone(), token.clone(), stop.clone());
thread::spawn(move || fetch_waves(core, base, pin, token, wrx, &stop));
}
let st = stop.clone();
thread::spawn(move || s.run(&st, repaint));
Link { stop, state, session, token, new_password }
Expand Down Expand Up @@ -458,6 +467,9 @@ struct Syncer {
log_rev: u64,
hist_rev: u64,
lib_ver: u64,
miss_rev: u64,
/// the server's clock minus this machine's (ms), from the quickest poll lately: (offset, round trip, when)
clock: Option<(i64, u32, Instant)>,
/// fingerprints of the mirror's playlists / station settings as last agreed with the server
/// (None until the first copy arrives, and after a clash: the server's copy then wins)
tabs_hash: Option<u64>,
Expand Down Expand Up @@ -605,8 +617,20 @@ impl Syncer {
}
}
// 3. what the server has now
let snap: Snapshot =
self.get(&format!("state?rev={}&cfg={}&log={}&hist={}", self.rev, self.cfg_rev, self.log_rev, self.hist_rev))?;
let sent = chrono::Utc::now().timestamp_millis();
let t0 = Instant::now();
let snap: Snapshot = self.get(&format!(
"state?rev={}&cfg={}&log={}&hist={}&miss={}",
self.rev, self.cfg_rev, self.log_rev, self.hist_rev, self.miss_rev
))?;
if snap.live.now_ms > 0 {
// the server read its clock about halfway through the round trip
let rtt = t0.elapsed().as_millis() as u32;
let offset = snap.live.now_ms - (sent + rtt as i64 / 2);
if self.clock.is_none_or(|(_, best, at)| rtt <= best || at.elapsed() > Duration::from_secs(30)) {
self.clock = Some((offset, rtt, Instant::now()));
}
}
let library = if snap.lib_ver != self.lib_ver { Some(self.get::<Vec<crate::library::Track>>("library")?) } else { None };
if stop.load(Ordering::Relaxed) {
return Ok(());
Expand All @@ -616,6 +640,7 @@ impl Syncer {
return Ok(()); // disconnected meanwhile: this is the local playout again
}
let notify = snap.live.notify.clone();
c.clock_offset_ms = self.clock.map(|(o, _, _)| o).unwrap_or(0);
c.apply_live(snap.live);
if notify != self.notify {
*c.notify.lock() = notify.clone();
Expand Down Expand Up @@ -648,6 +673,52 @@ impl Syncer {
c.set_library(lib);
self.lib_ver = snap.lib_ver;
}
if let Some(m) = snap.missing {
c.set_missing(m);
self.miss_rev = snap.missing_rev;
}
Ok(())
}
}

/// How long an app waits for the server to analyse one track.
const WAVE_WAIT: Duration = Duration::from_secs(180);

/// Ask the server for the waveforms the track editor wants, one file at a time.
fn fetch_waves(
core: Arc<Mutex<Playout>>,
base: String,
pin: String,
token: Arc<Mutex<String>>,
asks: Receiver<std::path::PathBuf>,
stop: &AtomicBool,
) {
let agent = agent(&pin);
while !stop.load(Ordering::Relaxed) {
let path = match asks.recv_timeout(Duration::from_millis(500)) {
Ok(p) => p,
Err(RecvTimeoutError::Timeout) => continue,
Err(RecvTimeoutError::Disconnected) => return,
};
let since = Instant::now();
let r = loop {
if stop.load(Ordering::Relaxed) {
return;
}
let tok = token.lock().clone();
match call::<WaveReply>(&agent, &base, &tok, "wave", Some(serde_json::json!({ "path": path }))) {
Ok(WaveReply { wave: Some(w), .. }) => break Ok(w),
Ok(_) if since.elapsed() < WAVE_WAIT => thread::sleep(Duration::from_millis(700)),
Ok(_) => break Err("the server took too long to analyse this track".to_string()),
// signed out for a moment (the server restarted): the syncer signs in again
Err(Fail::Auth(_)) if since.elapsed() < WAVE_WAIT => thread::sleep(Duration::from_secs(1)),
Err(Fail::Other(e)) if e == "unknown call" => break Err("update the server to see waveforms here".to_string()),
Err(e) => break Err(e.text()),
}
};
let mut c = core.lock();
if c.link.is_some() {
c.put_remote_wave(path, r);
}
}
}
7 changes: 4 additions & 3 deletions src/panes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ impl App {
let is_cur = on_air && playing && cur == Some(i);
row.set_selected(sel.contains(&i));
let st = starts.get(i).copied().flatten();
let col = row_text_color(e, is_cur, st.is_some_and(|s| s.skipped), dark);
let col = row_text_color(e, is_cur, st.is_some_and(|s| s.skipped), dark, core.row_missing(e));
row.col(|ui| {
row_background(ui, e, is_cur);
ui.label(RichText::new(if is_cur { "▶".to_string() } else { (i + 1).to_string() }).color(col));
Expand Down Expand Up @@ -754,7 +754,8 @@ pub(super) fn ui_dark(w: &WorkzoneSet) -> bool {
}

/// Text colour of a playlist row.
pub(super) fn row_text_color(e: &PlEntry, is_cur: bool, skipped: bool, dark: bool) -> Color32 {
/// `missing`: the file isn't there (on the server, when connected to one).
pub(super) fn row_text_color(e: &PlEntry, is_cur: bool, skipped: bool, dark: bool, missing: bool) -> Color32 {
if matches!(e.kind, ItemKind::HourInfo { .. }) {
return if dark { Color32::from_rgb(255, 150, 200) } else { Color32::from_rgb(170, 30, 110) };
}
Expand All @@ -765,7 +766,7 @@ pub(super) fn row_text_color(e: &PlEntry, is_cur: bool, skipped: bool, dark: boo
if skipped || e.played {
return Color32::from_gray(140);
}
if e.is_file() && crate::library::file_missing(&e.track.path) {
if missing {
return Color32::from_rgb(200, 50, 50);
}
match e.kind {
Expand Down
Loading
Loading