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
15 changes: 11 additions & 4 deletions app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use std::sync::{Arc, Mutex};
use palaclips_engine::EngineConfig;
use palaclips_engine::engine::{Engine, EngineHandle, LevelRate, track_descriptors};
use palaclips_engine::events::{
CAPTURE_ALERTS, EngineEvent, EngineState, Localized, Severity, SourceMode, TrackDescriptor,
TrackKind, TrackState,
CAPTURE_ALERTS, EngineEvent, EngineState, Localized, SaveOrigin, Severity, SourceMode,
TrackDescriptor, TrackKind, TrackState,
};
use serde::Serialize;
use tauri::menu::{Menu, MenuItem};
Expand Down Expand Up @@ -574,12 +574,18 @@ impl EngineSlot {
EngineEvent::ClipSaved {
path,
duration_secs,
origin,
..
} => {
self.library_changed(app, &[std::path::PathBuf::from(path)]);
let feedback = self.feedback();
if feedback.sound {
feedback::play(feedback::Sound::Saved);
let sound = match origin {
SaveOrigin::Replay => Some(feedback::Sound::Saved),
SaveOrigin::PostRoll => Some(feedback::Sound::ClipFinished),
SaveOrigin::Recording => None,
};
if let Some(sound) = sound.filter(|_| feedback.sound) {
feedback::play(sound);
}
if feedback.toast {
let game = std::path::Path::new(path)
Expand Down Expand Up @@ -1306,6 +1312,7 @@ mod tests {
duration_secs: 12.0,
save_latency_ms: 20,
thumbnail: None,
origin: SaveOrigin::Replay,
});
assert!(status.alerts.is_empty());
}
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,7 @@ export type EngineEvent =
duration_secs: number;
save_latency_ms: number;
thumbnail: string | null;
origin: "replay" | "post_roll" | "recording";
}
| { type: "audio_levels"; levels: number[] }
| { type: "replay_stats"; buffered_secs: number; bytes: number }
Expand Down
10 changes: 7 additions & 3 deletions crates/cli/src/replay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use palaclips_engine::config::{
AppTrack, AudioSettings, RecordSettings, RecordSplit, ReplaySettings, ReplayStorage,
};
use palaclips_engine::engine::{Engine, LevelRate};
use palaclips_engine::events::{EngineEvent, EngineState};
use palaclips_engine::events::{EngineEvent, EngineState, SaveOrigin};
use palaclips_engine::paths::private_bytes;

const STATS_EVERY: u32 = 5;
Expand Down Expand Up @@ -253,10 +253,14 @@ pub fn run_replay(args: &ReplayArgs) -> Result<()> {
recv(engine.events()) -> event => {
let Ok(event) = event else { break };
match event {
EngineEvent::ClipSaved { path, duration_secs, save_latency_ms, .. } => {
EngineEvent::ClipSaved { path, duration_secs, save_latency_ms, origin, .. } => {
finished += 1;
latencies.push(save_latency_ms);
chime_on(Sound::Saved);
match origin {
SaveOrigin::Replay => chime_on(Sound::Saved),
SaveOrigin::PostRoll => chime_on(Sound::ClipFinished),
SaveOrigin::Recording => {}
}
println!(
"[{:>6} s] clip {finished} : {duration_secs:.3} s, prêt en {save_latency_ms} ms ; mémoire privée {:.1} Mo → {path}",
started.elapsed().as_secs(),
Expand Down
47 changes: 40 additions & 7 deletions crates/engine/src/chime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub enum Sound {
Saved,
RecordingStarted,
RecordingStopped,
ClipFinished,
}

impl Sound {
Expand All @@ -17,6 +18,11 @@ impl Sound {
Self::Saved => &[(880.0, 0.0, 0.11), (1318.5, 0.075, 0.2)],
Self::RecordingStarted => &[(523.25, 0.0, 0.1), (783.99, 0.075, 0.2)],
Self::RecordingStopped => &[(783.99, 0.0, 0.1), (523.25, 0.075, 0.2)],
Self::ClipFinished => &[
(880.0, 0.0, 0.1),
(1318.5, 0.075, 0.1),
(1760.0, 0.15, 0.22),
],
}
}

Expand All @@ -25,6 +31,7 @@ impl Sound {
Self::Saved => 0,
Self::RecordingStarted => 1,
Self::RecordingStopped => 2,
Self::ClipFinished => 3,
}
}
}
Expand Down Expand Up @@ -80,7 +87,7 @@ pub fn play(sound: Sound) {
use windows::Win32::Media::Audio::{PlaySoundW, SND_ASYNC, SND_MEMORY, SND_NODEFAULT};
use windows::core::PCWSTR;

static SOUNDS: [OnceLock<Vec<u8>>; 3] = [OnceLock::new(), OnceLock::new(), OnceLock::new()];
static SOUNDS: [OnceLock<Vec<u8>>; 4] = [const { OnceLock::new() }; 4];
let chime = SOUNDS[sound.index()].get_or_init(|| wav(&sound_samples(sound.notes())));
// SAFETY: SND_MEMORY reads the WAV from `chime`, a static buffer that outlives the
// asynchronous playback.
Expand All @@ -100,13 +107,39 @@ pub fn play(sound: Sound) {
mod tests {
use super::*;

const ALL: [Sound; 4] = [
Sound::Saved,
Sound::RecordingStarted,
Sound::RecordingStopped,
Sound::ClipFinished,
];

#[test]
fn all_sounds_differ() {
let samples: Vec<_> = ALL.map(|sound| sound_samples(sound.notes())).into();
for (index, sound) in samples.iter().enumerate() {
for other in &samples[index + 1..] {
assert_ne!(sound, other);
}
}
let indices: std::collections::HashSet<_> = ALL.map(Sound::index).into();
assert_eq!(indices.len(), ALL.len());
}

#[test]
fn recording_sounds_differ_from_the_save_chime() {
let saved = sound_samples(Sound::Saved.notes());
let started = sound_samples(Sound::RecordingStarted.notes());
let stopped = sound_samples(Sound::RecordingStopped.notes());
assert_ne!(saved, started);
assert_ne!(started, stopped);
fn the_finished_clip_sound_keeps_the_timbre_and_volume_of_the_chime() {
let peak = |sound: Sound| {
sound_samples(sound.notes())
.iter()
.map(|sample| sample.unsigned_abs())
.max()
.unwrap_or(0)
};
let saved = f32::from(peak(Sound::Saved));
let finished = f32::from(peak(Sound::ClipFinished));
assert!((finished / saved - 1.0).abs() < 0.15, "{saved} {finished}");
let seconds = sound_samples(Sound::ClipFinished.notes()).len() as f32 / SAMPLE_RATE as f32;
assert!(seconds < 0.4, "{seconds}");
}

#[test]
Expand Down
5 changes: 3 additions & 2 deletions crates/engine/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ use crate::encode::{
};
use crate::error::{EngineError, Result};
use crate::events::{
CapturePerformance, EngineEvent, EngineState, Localized, Severity, SourceMode, TrackDescriptor,
TrackKind,
CapturePerformance, EngineEvent, EngineState, Localized, SaveOrigin, Severity, SourceMode,
TrackDescriptor, TrackKind,
};
use crate::library::ensure_free_space;
use crate::mux::{Mp4Writer, MuxStream};
Expand Down Expand Up @@ -1828,6 +1828,7 @@ impl SaveJob {
duration_secs: self.duration.as_secs_f64(),
save_latency_ms: (latency.0 / 10_000) as u64,
thumbnail,
origin: SaveOrigin::Replay,
})
}
}
Expand Down
9 changes: 9 additions & 0 deletions crates/engine/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ pub enum TrackState {
Unavailable,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SaveOrigin {
Replay,
PostRoll,
Recording,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct SourceMode {
pub width: u32,
Expand Down Expand Up @@ -129,6 +137,7 @@ pub enum EngineEvent {
duration_secs: f64,
save_latency_ms: u64,
thumbnail: Option<String>,
origin: SaveOrigin,
},
AudioLevels {
levels: Vec<f32>,
Expand Down
19 changes: 15 additions & 4 deletions crates/engine/src/record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::clock::{HNS_PER_SECOND, Hns};
use crate::config::RecordSplit;
use crate::encode::{route_ffmpeg_logs, write_thumbnail};
use crate::error::{EngineError, Result};
use crate::events::{EngineEvent, Localized, Severity};
use crate::events::{EngineEvent, Localized, SaveOrigin, Severity};
use crate::library::{STOP_FREE_BYTES, WARN_FREE_BYTES, disk_space};
use crate::mux::{Mp4Writer, MuxStream};
use crate::paths;
Expand Down Expand Up @@ -347,7 +347,12 @@ impl Segment {
self.writer.write(stream, packet)
}

fn finish(self, layout: &RecordLayout, journal: &Journal) -> Result<EngineEvent> {
fn finish(
self,
layout: &RecordLayout,
journal: &Journal,
kind: RecordKind,
) -> Result<EngineEvent> {
self.writer.finish()?;
journal.remove(&self.path)?;
let duration = self.newest_video + layout.frame - self.start;
Expand All @@ -371,6 +376,10 @@ impl Segment {
duration_secs: duration.as_secs_f64(),
save_latency_ms: 0,
thumbnail,
origin: match kind {
RecordKind::Recording => SaveOrigin::Recording,
RecordKind::Replay => SaveOrigin::PostRoll,
},
})
}
}
Expand Down Expand Up @@ -457,15 +466,17 @@ impl Writer {
fn close_previous(&mut self) -> Result<()> {
self.splitter.close_previous();
if let Some(previous) = self.previous.take() {
let event = previous.finish(&self.layout, &self.journal)?;
let event = previous.finish(&self.layout, &self.journal, self.target.kind)?;
self.events.try_send(event).ok();
}
Ok(())
}

fn finish(mut self) -> Result<()> {
let previous = self.close_previous();
let event = self.current.finish(&self.layout, &self.journal)?;
let event = self
.current
.finish(&self.layout, &self.journal, self.target.kind)?;
self.events.try_send(event).ok();
previous
}
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ aussi l'`AppStatus` renvoyé par `get_state`.
`tracks_renamed { tracks: [{ title, kind }] }` (nom de piste ou langue changés pendant le
replay : titres mis à jour sans redémarrage, appliqués au prochain clip), `game_named { exe_name, name }` (nom de jeu choisi et figé, ADR-024 ; l'app l'ajoute à
`settings.json`), `track_changed { track, title, state: waiting | capturing | silent | unavailable, source }` (appli ou
périphérique rattaché ; émis à chaque changement), `clip_saved`, `audio_levels { levels }` (crête linéaire 0–1 de chaque piste depuis
périphérique rattaché ; émis à chaque changement), `clip_saved { path, duration_secs, save_latency_ms, thumbnail, origin }` (`origin` = `replay`, `post_roll` ou `recording` : le shell joue le carillon pour un replay, le son « clip terminé » à la fin d'un « après », rien pour un enregistrement, qui a déjà son son de fin), `audio_levels { levels }` (crête linéaire 0–1 de chaque piste depuis
le relevé précédent, un nombre par piste dans l'ordre des pistes : le front indexe par numéro
de piste, jamais par titre ; 20 Hz fenêtre focalisée, 10 Hz sans focus, coupé
fenêtre réduite ou fermée : `EngineHandle::set_level_rate`, piloté par les événements de la
Expand Down
2 changes: 1 addition & 1 deletion docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ sans lui, encodage par le processeur (ENC-06, v1.1).
| RPL-07 | Changer de sources ou de zone redémarre le buffer ; l'UI le dit avant d'appliquer. |
| RPL-08 | RAM stable en replay armé : pas de croissance avec le temps ni avec le nombre de sauvegardes. |
| RPL-09 | Deux sauvegardes successives ne se répètent pas : le clip suivant commence où le précédent s'est arrêté (au plus un intervalle d'images clés en commun) ; « En mémoire » repart de zéro après chaque sauvegarde. Moins de 3 s en mémoire : pas de clip, message discret dans l'app (ni notification ni son en jeu). |
| RPL-10 | **Durée « après »** (v1.2) : 0 s par défaut, préréglages 10 s, 30 s, 1 min, 5 min ou valeur libre jusqu'à 10 min, appliquée au raccourci de sauvegarde. L'avant est écrit tout de suite dans un MP4 hybride, puis le direct y est ajouté jusqu'à la fin de la durée (aucune RAM en plus, fichier récupérable après un arrêt brutal). Carillon et notification « Clip en cours, +X s » à l'appui, « Clip sauvegardé » à la fin ; un nouvel appui prolonge le même clip ; « Terminer maintenant » sur État et dans la zone de notification. Pas de trou ni d'image en double à la jonction, synchro A/V au niveau des clips actuels. |
| RPL-10 | **Durée « après »** (v1.2) : 0 s par défaut, préréglages 10 s, 30 s, 1 min, 5 min ou valeur libre jusqu'à 10 min, appliquée au raccourci de sauvegarde. L'avant est écrit tout de suite dans un MP4 hybride, puis le direct y est ajouté jusqu'à la fin de la durée (aucune RAM en plus, fichier récupérable après un arrêt brutal). Carillon et notification « Clip en cours, +X s » à l'appui, son « clip terminé » (v1.4, distinct du carillon et des sons d'enregistrement, même réglage « Son de confirmation ») et notification « Clip sauvegardé » à la fin ; un nouvel appui prolonge le même clip ; « Terminer maintenant » sur État et dans la zone de notification. Pas de trou ni d'image en double à la jonction, synchro A/V au niveau des clips actuels. |

### 2.5 Enregistrement manuel (REC)

Expand Down
1 change: 1 addition & 0 deletions scripts/find-chimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"sauvegarde": [(880.0, 0.0, 0.11), (1318.5, 0.075, 0.2)],
"début d'enregistrement": [(523.25, 0.0, 0.1), (783.99, 0.075, 0.2)],
"fin d'enregistrement": [(783.99, 0.0, 0.1), (523.25, 0.075, 0.2)],
"clip terminé": [(880.0, 0.0, 0.1), (1318.5, 0.075, 0.1), (1760.0, 0.15, 0.22)],
}


Expand Down
Loading