mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-07 18:42:10 +00:00
enhanced behavior
This commit is contained in:
1 parent
12f7f56928
commit
4eb6f16123
6 files changed
+61
-14
No files matched your search
@@ -55,6 +55,9 @@ extra_alert_await_sounds:
|
||||
- smrpg_drybones_crumble.wav
|
||||
|
||||
loop_alert_timeout_millis: 2000
|
||||
loop_alert_timeout_millis_1: 1000
|
||||
loop_alert_timeout_millis_2: 500
|
||||
loop_alert_timeout_millis_3: 200
|
||||
escalate_wait_1: 15
|
||||
escalate_wait_2: 30
|
||||
escalate_wait_3: 45
|
||||
@@ -63,7 +66,10 @@ escalate_wait_3: 45
|
||||
- Paths can be absolute, or relative to the YAML file's directory.
|
||||
- WAV and MP3 are both supported (decoded via rodio + symphonia).
|
||||
- `loop_alert_timeout_millis` is the gap *between* consecutive plays of a
|
||||
single voice. Omit to replay back-to-back.
|
||||
single voice at stage 0. Omit to replay back-to-back.
|
||||
- `loop_alert_timeout_millis_{1,2,3}` override the gap at each escalation
|
||||
stage. Each falls back to the previous stage when unset — so you can set
|
||||
just `_1: 200` to drop straight to a 200ms gap from voice 2 onward.
|
||||
- **Escalation**: a `/loop_*` request starts one voice immediately. At
|
||||
`escalate_wait_1` / `escalate_wait_2` / `escalate_wait_3` seconds, a
|
||||
second / third / fourth concurrent voice joins the mix. New voices are
|
||||
@@ -71,6 +77,8 @@ escalate_wait_3: 45
|
||||
exhausted the supervisor cycles back through the full pool
|
||||
(primary + extras), so existing voices double up. Each voice runs in
|
||||
its own thread and drifts naturally relative to the others.
|
||||
- All voices in a session share the *current* stage's gap, so existing
|
||||
voices also speed up when the supervisor advances stages.
|
||||
|
||||
## Claude Code wiring
|
||||
|
||||
|
||||
@@ -30,6 +30,12 @@ extra_alert_await_sounds:
|
||||
- /Users/bt/dev/storyteller/artcraft/frontend/apps/artcraft/app/public/resources/sound/smrpg_enemy_spikethrow.wav
|
||||
|
||||
loop_alert_timeout_millis: 2000
|
||||
# Optional per-stage gaps. Each falls back to the previous stage when unset,
|
||||
# so leaving them out keeps the loop at `loop_alert_timeout_millis` the whole
|
||||
# time; specifying them lets the loop accelerate as it escalates.
|
||||
loop_alert_timeout_millis_1: 1000
|
||||
loop_alert_timeout_millis_2: 500
|
||||
loop_alert_timeout_millis_3: 200
|
||||
|
||||
# Seconds-from-loop-start at which voices 2, 3, and 4 join the mix.
|
||||
escalate_wait_1: 15
|
||||
|
||||
@@ -25,7 +25,7 @@ use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -63,8 +63,11 @@ pub struct LoopSpec {
|
||||
/// indexes this with `layer % pool.len()`, so when extras run out it
|
||||
/// cycles back through the pool (doubling already-playing voices).
|
||||
pub pool: Vec<PathBuf>,
|
||||
/// Gap between consecutive plays of a single iterator's sound.
|
||||
pub gap_millis: u64,
|
||||
/// Gap (millis) between consecutive plays at each escalation stage:
|
||||
/// [initial, after wait_1, after wait_2, after wait_3]. All iterators in
|
||||
/// the session share the current stage's value, so existing voices also
|
||||
/// speed up when the supervisor moves to a faster stage.
|
||||
pub gap_millis_schedule: [u64; 4],
|
||||
/// Wall-clock seconds from loop start when layers 2, 3, 4 should join.
|
||||
pub escalate_waits_secs: [u64; 3],
|
||||
}
|
||||
@@ -170,11 +173,14 @@ fn run_loop_supervisor(
|
||||
return;
|
||||
}
|
||||
|
||||
let schedule = spec.gap_millis_schedule;
|
||||
let gap_millis = Arc::new(AtomicU64::new(schedule[0]));
|
||||
|
||||
let mut iterators: Vec<JoinHandle<()>> = Vec::with_capacity(4);
|
||||
iterators.push(spawn_iterator(
|
||||
&stream_handle,
|
||||
spec.pool[0].clone(),
|
||||
spec.gap_millis,
|
||||
gap_millis.clone(),
|
||||
stop.clone(),
|
||||
0,
|
||||
));
|
||||
@@ -193,11 +199,15 @@ fn run_loop_supervisor(
|
||||
if !sleep_with_stop(Duration::from_secs(intervals[layer]), &stop) {
|
||||
break;
|
||||
}
|
||||
let next_gap = schedule[layer + 1];
|
||||
if gap_millis.swap(next_gap, Ordering::Relaxed) != next_gap {
|
||||
log::info!("escalation stage {}: gap now {}ms", layer + 1, next_gap);
|
||||
}
|
||||
let pool_idx = (layer + 1) % pool_len;
|
||||
iterators.push(spawn_iterator(
|
||||
&stream_handle,
|
||||
spec.pool[pool_idx].clone(),
|
||||
spec.gap_millis,
|
||||
gap_millis.clone(),
|
||||
stop.clone(),
|
||||
layer + 1,
|
||||
));
|
||||
@@ -218,7 +228,7 @@ fn run_loop_supervisor(
|
||||
fn spawn_iterator(
|
||||
stream_handle: &OutputStreamHandle,
|
||||
path: PathBuf,
|
||||
gap_millis: u64,
|
||||
gap_millis: Arc<AtomicU64>,
|
||||
stop: Arc<AtomicBool>,
|
||||
layer: usize,
|
||||
) -> JoinHandle<()> {
|
||||
@@ -233,7 +243,7 @@ fn spawn_iterator(
|
||||
fn run_loop_iterator(
|
||||
stream_handle: OutputStreamHandle,
|
||||
path: PathBuf,
|
||||
gap_millis: u64,
|
||||
gap_millis: Arc<AtomicU64>,
|
||||
stop: Arc<AtomicBool>,
|
||||
) {
|
||||
let sink = match Sink::try_new(&stream_handle) {
|
||||
@@ -262,9 +272,8 @@ fn run_loop_iterator(
|
||||
return;
|
||||
}
|
||||
|
||||
if gap_millis > 0
|
||||
&& !sleep_with_stop(Duration::from_millis(gap_millis), &stop)
|
||||
{
|
||||
let gap = gap_millis.load(Ordering::Relaxed);
|
||||
if gap > 0 && !sleep_with_stop(Duration::from_millis(gap), &stop) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,13 @@ pub struct NotifyConfig {
|
||||
pub extra_alert_await_sounds: Vec<PathBuf>,
|
||||
|
||||
pub loop_alert_timeout_millis: Option<u64>,
|
||||
/// Gap (millis) between plays once the loop has been running for
|
||||
/// `escalate_wait_1` seconds. Falls back to the previous stage.
|
||||
pub loop_alert_timeout_millis_1: Option<u64>,
|
||||
/// Gap (millis) after `escalate_wait_2`. Falls back to stage 1.
|
||||
pub loop_alert_timeout_millis_2: Option<u64>,
|
||||
/// Gap (millis) after `escalate_wait_3`. Falls back to stage 2.
|
||||
pub loop_alert_timeout_millis_3: Option<u64>,
|
||||
|
||||
/// Seconds after a loop starts before a second concurrent voice is added.
|
||||
pub escalate_wait_1: Option<u64>,
|
||||
@@ -73,6 +80,18 @@ impl NotifyConfig {
|
||||
]
|
||||
}
|
||||
|
||||
/// Per-stage gap (millis) between plays of a single voice. Each entry
|
||||
/// falls back to the previous stage when unset, so a config that only
|
||||
/// specifies `loop_alert_timeout_millis_1: 200` will use that 200ms gap
|
||||
/// for stages 2 and 3 too.
|
||||
pub fn loop_gap_schedule_millis(&self) -> [u64; 4] {
|
||||
let g0 = self.loop_alert_timeout_millis.unwrap_or(0);
|
||||
let g1 = self.loop_alert_timeout_millis_1.unwrap_or(g0);
|
||||
let g2 = self.loop_alert_timeout_millis_2.unwrap_or(g1);
|
||||
let g3 = self.loop_alert_timeout_millis_3.unwrap_or(g2);
|
||||
[g0, g1, g2, g3]
|
||||
}
|
||||
|
||||
fn resolve_relative_paths(&mut self, base: &Path) {
|
||||
resolve_opt(&mut self.alert_beep_sound, base);
|
||||
resolve_opt(&mut self.alert_done_sound, base);
|
||||
|
||||
@@ -48,7 +48,7 @@ fn play_loop_or_404(
|
||||
|
||||
let spec = LoopSpec {
|
||||
pool,
|
||||
gap_millis: state.config.loop_alert_timeout_millis.unwrap_or(0),
|
||||
gap_millis_schedule: state.config.loop_gap_schedule_millis(),
|
||||
escalate_waits_secs: state.config.escalate_waits_secs(),
|
||||
};
|
||||
state.audio.play_loop(spec);
|
||||
|
||||
@@ -42,7 +42,10 @@
|
||||
</table>
|
||||
<p style="color:#555;font-size:14px;">
|
||||
Between iterations a single voice waits <code>loop_alert_timeout_millis</code>
|
||||
before replaying.
|
||||
before replaying. Stages 1–3 can override via
|
||||
<code>loop_alert_timeout_millis_{1,2,3}</code> — each falls back to the
|
||||
previous stage if unset, so the loop progressively speeds up as it
|
||||
escalates.
|
||||
</p>
|
||||
<p style="color:#555;font-size:14px;">
|
||||
<strong>Escalation:</strong> after the loop starts, a second voice joins
|
||||
@@ -50,7 +53,9 @@
|
||||
<code>escalate_wait_2</code>, and a fourth at <code>escalate_wait_3</code>.
|
||||
Each new voice is drawn from <code>extra_alert_<state>_sounds</code>;
|
||||
when those run out the pool cycles, doubling already-playing voices.
|
||||
Voices are independent threads — they drift relative to one another.
|
||||
All voices in a session share the current stage's gap, so existing
|
||||
voices speed up too when the supervisor advances. Voices are
|
||||
independent threads — they drift relative to one another.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user