- the quest giver's dialog and the quest log are the reference's own `QuestFrame.xml` and `QuestLogFrame.xml`, run off the player's chain at the reference's own toc seats; our 1366-line and 1999-line copies are deleted, and the verbs the stock files call are built — the four `GameTooltip` quest-item and reward-spell setters, the reward spell itself (parsed on all three packets and dropped by the model until now), and two verbs; the detail pane could never scroll because `UpdateScrollChildRect` measured a frame late where the reference's measure is synchronous, so it measures through the installed font engine now; and ten things the reference does that ours did not, each found by a test that had enshrined ours — a shift-click never selects, an empty log hides the whole detail pane and says "No Active Quests", the abandon confirm lives in the popup registry (1944) - the quest bindings' law, read off the binary and folded back: no 1.12 packet carries a quest background material — `GetQuestBackgroundMaterial` (`0x502230`) forks on the dialog's SOURCE object, an item answering its template's PageMaterial, a GameObject its template data, a creature nil (FrameXML's "Parchment"), the id naming a `PageTextMaterial.dbc` row whose six words are nowhere in the binary; its twin `ItemTextGetMaterial` has the same body, so the reader window already held every piece and the giver's feed runs the same resolve; `QuestChooseRewardError` is game-error row `0x98`, and the row plays `igQuestFailed` first, which the verb now queues; the reward-spell getters' third return is `Spell.dbc` Attributes bit 5 (1161 of 22357 rows set — craft-cast spells set, `Pattern:` teaching spells clear), read off the catalog now (1946) - the "20 ms frames" of a raid, chased to their number: the probe's worst frames now carry their churn (entities, assets, first-play sound decodes, live particles, the GPU reading) and in a Blackrock raid with sound on every one of those read flat; then the same window on the PACED clock — under vsync with pipelined rendering the raw delta is mis-split into long/short pairs that sum to exact refresh multiples, so an alone Stormwind leg read 115 of 300 frames over budget with cpu and gpu both at their means, while the paced clock read 0 skipped frames in every leg; what remains real in the raid is `cpu_p99` at 22–23 ms with sound on; and every submesh the streamer spawns now carries why it is an entity, counted per reason — 5.8k resident ADT-doodad submeshes at the Stormwind auction house, 5.3k of them hidden, three prop families the retained pass declines by rule, each riding every per-`Mesh3d` sweep for ~0.4 ms of the alone frame (1947) - a raid's dozen voices cost the mixer ~1.5 ms of every 60 Hz frame: upstream kira spatializes every frame — normalizes, the attenuation curve and a dB→amplitude `powf` per frame per spatial track, two more `powf`s for the volume tween, ~1.7 M `powf`s a second at 48 kHz × 12 tracks for gains that change imperceptibly across a chunk — so kira 0.12.1 is vendored with one patch: the spatial gains and the volume × fade amplitude are evaluated at the chunk's two ends and interpolated across its ~10.7 ms, the granularity FMOD 3, the reference's own mixer, updates them at; nothing a listener can name changes (1945); and rig poses are evaluated in parallel over rigs — each reads its shared clips and writes only its own pose, and a raid's ~400 live rigs were 0.4 ms of one thread walked serially on the main thread's critical path (1947) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
356 lines
8.7 KiB
Rust
356 lines
8.7 KiB
Rust
/*!
|
|
Precise timing for audio events.
|
|
|
|
Clocks can be used to set the start times of sounds and tweens. To create a
|
|
clock, use [`AudioManager::add_clock`](crate::AudioManager::add_clock).
|
|
|
|
```no_run
|
|
use kira::{
|
|
AudioManager, AudioManagerSettings, DefaultBackend,
|
|
clock::ClockSpeed,
|
|
};
|
|
|
|
let mut manager = AudioManager::<DefaultBackend>::new(AudioManagerSettings::default())?;
|
|
let mut clock = manager.add_clock(ClockSpeed::SecondsPerTick(1.0))?;
|
|
clock.start();
|
|
# Result::<(), Box<dyn std::error::Error>>::Ok(())
|
|
```
|
|
|
|
You can specify the speed of the clock as seconds per tick, ticks per second, or
|
|
ticks per minute.
|
|
|
|
Clocks are stopped when you first create them, so be sure to explicitly call
|
|
[`ClockHandle::start`] when you want the clock to start ticking.
|
|
|
|
## Starting sounds on clock ticks
|
|
|
|
Sounds can be set to only start playing when a clock has ticked a certain
|
|
number of times. You can configure this using
|
|
[`StaticSoundData::start_time`](crate::sound::static_sound::StaticSoundData::start_time)
|
|
or [`StreamingSoundData::start_time`](crate::sound::streaming::StreamingSoundData::start_time).
|
|
|
|
```no_run
|
|
use kira::{
|
|
clock::{ClockTime, ClockSpeed},
|
|
AudioManager, AudioManagerSettings, DefaultBackend,
|
|
sound::static_sound::{StaticSoundData, StaticSoundSettings},
|
|
StartTime,
|
|
};
|
|
|
|
let mut manager = AudioManager::<DefaultBackend>::new(AudioManagerSettings::default())?;
|
|
let mut clock = manager.add_clock(ClockSpeed::SecondsPerTick(1.0))?;
|
|
manager.play(
|
|
StaticSoundData::from_file("sound.ogg")?
|
|
.start_time(StartTime::ClockTime(ClockTime {
|
|
clock: clock.id(),
|
|
ticks: 4,
|
|
fraction: 0.0,
|
|
}))
|
|
)?;
|
|
clock.start();
|
|
# Result::<(), Box<dyn std::error::Error>>::Ok(())
|
|
```
|
|
|
|
As a shorthand, you can pass the [`ClockTime`] directly into
|
|
the `start_time` function.
|
|
|
|
```no_run
|
|
# use kira::{
|
|
# clock::{ClockTime, ClockSpeed},
|
|
# AudioManager,
|
|
# AudioManagerSettings,
|
|
# DefaultBackend,
|
|
# sound::static_sound::{StaticSoundData, StaticSoundSettings},
|
|
# StartTime,
|
|
# };
|
|
#
|
|
# let mut manager = AudioManager::<DefaultBackend>::new(
|
|
# AudioManagerSettings::default(),
|
|
# )?;
|
|
# let mut clock = manager.add_clock(ClockSpeed::SecondsPerTick(1.0))?;
|
|
manager.play(
|
|
StaticSoundData::from_file("sound.ogg")?
|
|
.start_time(ClockTime {
|
|
clock: clock.id(),
|
|
ticks: 4,
|
|
fraction: 0.0,
|
|
})
|
|
)?;
|
|
# clock.start();
|
|
# Result::<(), Box<dyn std::error::Error>>::Ok(())
|
|
```
|
|
|
|
As an even shorter hand, you can use [`ClockHandle::time`] to get the clock's
|
|
current [`ClockTime`], and then add to it to get a time in the future:
|
|
|
|
```no_run
|
|
use kira::{
|
|
AudioManager, AudioManagerSettings, DefaultBackend,
|
|
sound::static_sound::{StaticSoundData, StaticSoundSettings},
|
|
clock::ClockSpeed,
|
|
};
|
|
|
|
# let mut manager = AudioManager::<DefaultBackend>::new(
|
|
# AudioManagerSettings::default(),
|
|
# )?;
|
|
# let mut clock = manager.add_clock(ClockSpeed::SecondsPerTick(1.0))?;
|
|
manager.play(
|
|
StaticSoundData::from_file("sound.ogg")?
|
|
.start_time(clock.time() + 4)
|
|
)?;
|
|
# clock.start();
|
|
# Result::<(), Box<dyn std::error::Error>>::Ok(())
|
|
```
|
|
|
|
## Starting tweens on clock ticks
|
|
|
|
You can also use clocks to set the start time of tweens. In this example, we set
|
|
the playback rate of a sound to start tweening when a clock reaches its third
|
|
tick.
|
|
|
|
```no_run
|
|
use std::time::Duration;
|
|
|
|
use kira::{
|
|
AudioManager, AudioManagerSettings, DefaultBackend,
|
|
sound::static_sound::{StaticSoundData, StaticSoundSettings},
|
|
Tween,
|
|
clock::ClockSpeed,
|
|
StartTime,
|
|
};
|
|
|
|
let mut manager = AudioManager::<DefaultBackend>::new(AudioManagerSettings::default())?;
|
|
let mut clock = manager.add_clock(ClockSpeed::SecondsPerTick(1.0))?;
|
|
let mut sound = manager.play(StaticSoundData::from_file("sound.ogg")?)?;
|
|
sound.set_playback_rate(
|
|
0.5,
|
|
Tween {
|
|
start_time: StartTime::ClockTime(clock.time() + 3),
|
|
duration: Duration::from_secs(2),
|
|
..Default::default()
|
|
},
|
|
);
|
|
clock.start();
|
|
# Result::<(), Box<dyn std::error::Error>>::Ok(())
|
|
```
|
|
*/
|
|
|
|
mod clock_speed;
|
|
mod handle;
|
|
mod time;
|
|
|
|
#[cfg(test)]
|
|
mod test;
|
|
|
|
use atomic_arena::Key;
|
|
pub use clock_speed::*;
|
|
pub use handle::*;
|
|
pub use time::*;
|
|
|
|
use std::sync::{
|
|
Arc,
|
|
atomic::{AtomicBool, AtomicU64, Ordering},
|
|
};
|
|
|
|
use crate::{
|
|
Parameter, Value,
|
|
command::{ValueChangeCommand, read_commands_into_parameters},
|
|
command_writers_and_readers,
|
|
info::Info,
|
|
};
|
|
|
|
/// A unique identifier for a clock.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct ClockId(pub(crate) Key);
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct ClockShared {
|
|
ticking: AtomicBool,
|
|
ticks: AtomicU64,
|
|
fractional_position: AtomicU64,
|
|
removed: AtomicBool,
|
|
}
|
|
|
|
impl ClockShared {
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self {
|
|
ticking: AtomicBool::new(false),
|
|
ticks: AtomicU64::new(0),
|
|
fractional_position: AtomicU64::new(0.0f64.to_bits()),
|
|
removed: AtomicBool::new(false),
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn ticking(&self) -> bool {
|
|
self.ticking.load(Ordering::SeqCst)
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn ticks(&self) -> u64 {
|
|
self.ticks.load(Ordering::SeqCst)
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn fractional_position(&self) -> f64 {
|
|
f64::from_bits(self.fractional_position.load(Ordering::SeqCst))
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn is_marked_for_removal(&self) -> bool {
|
|
self.removed.load(Ordering::SeqCst)
|
|
}
|
|
|
|
pub fn mark_for_removal(&self) {
|
|
self.removed.store(true, Ordering::SeqCst);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub(crate) enum State {
|
|
NotStarted,
|
|
Started {
|
|
ticks: u64,
|
|
fractional_position: f64,
|
|
},
|
|
}
|
|
|
|
pub(crate) struct Clock {
|
|
command_readers: CommandReaders,
|
|
shared: Arc<ClockShared>,
|
|
ticking: bool,
|
|
speed: Parameter<ClockSpeed>,
|
|
state: State,
|
|
}
|
|
|
|
impl Clock {
|
|
#[must_use]
|
|
pub(crate) fn new(speed: Value<ClockSpeed>, id: ClockId) -> (Self, ClockHandle) {
|
|
let (command_writers, command_readers) = command_writers_and_readers();
|
|
let shared = Arc::new(ClockShared::new());
|
|
(
|
|
Self {
|
|
command_readers,
|
|
shared: shared.clone(),
|
|
ticking: false,
|
|
speed: Parameter::new(speed, ClockSpeed::TicksPerMinute(120.0)),
|
|
state: State::NotStarted,
|
|
},
|
|
ClockHandle {
|
|
id,
|
|
shared,
|
|
command_writers,
|
|
},
|
|
)
|
|
}
|
|
|
|
#[must_use]
|
|
pub(crate) fn without_handle(speed: Value<ClockSpeed>) -> Self {
|
|
let (_, command_readers) = command_writers_and_readers();
|
|
Self {
|
|
command_readers,
|
|
shared: Arc::new(ClockShared::new()),
|
|
ticking: false,
|
|
speed: Parameter::new(speed, ClockSpeed::TicksPerMinute(120.0)),
|
|
state: State::NotStarted,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub(crate) fn shared(&self) -> Arc<ClockShared> {
|
|
self.shared.clone()
|
|
}
|
|
|
|
#[must_use]
|
|
pub(crate) fn state(&self) -> State {
|
|
self.state
|
|
}
|
|
|
|
#[must_use]
|
|
pub(crate) fn ticking(&self) -> bool {
|
|
self.ticking
|
|
}
|
|
|
|
pub(crate) fn on_start_processing(&mut self) {
|
|
read_commands_into_parameters!(self, speed);
|
|
if let Some(ticking) = self.command_readers.set_ticking.read() {
|
|
self.set_ticking(ticking);
|
|
}
|
|
if self.command_readers.reset.read().is_some() {
|
|
self.reset();
|
|
}
|
|
self.update_shared();
|
|
}
|
|
|
|
fn set_ticking(&mut self, ticking: bool) {
|
|
self.ticking = ticking;
|
|
self.shared.ticking.store(ticking, Ordering::SeqCst);
|
|
}
|
|
|
|
fn reset(&mut self) {
|
|
self.state = State::NotStarted;
|
|
self.shared.ticks.store(0, Ordering::SeqCst);
|
|
}
|
|
|
|
fn update_shared(&mut self) {
|
|
let (ticks, fractional_position) = match &self.state {
|
|
State::NotStarted => (0, 0.0),
|
|
State::Started {
|
|
ticks,
|
|
fractional_position,
|
|
} => (*ticks, *fractional_position),
|
|
};
|
|
self.shared.ticks.store(ticks, Ordering::SeqCst);
|
|
self.shared
|
|
.fractional_position
|
|
.store(fractional_position.to_bits(), Ordering::SeqCst);
|
|
}
|
|
|
|
/// Updates the [`Clock`].
|
|
///
|
|
/// If the tick count changes this update, returns `Some(tick_number)`.
|
|
/// Otherwise, returns `None`.
|
|
pub(crate) fn update(&mut self, dt: f64, info: &Info) -> Option<u64> {
|
|
self.speed.update(dt, info);
|
|
if !self.ticking {
|
|
return None;
|
|
}
|
|
let mut new_tick_count = None;
|
|
if self.state == State::NotStarted {
|
|
self.state = State::Started {
|
|
ticks: 0,
|
|
fractional_position: 0.0,
|
|
};
|
|
new_tick_count = Some(0);
|
|
}
|
|
if let State::Started {
|
|
ticks,
|
|
fractional_position: tick_timer,
|
|
} = &mut self.state
|
|
{
|
|
*tick_timer += self.speed.value().as_ticks_per_second() * dt;
|
|
while *tick_timer >= 1.0 {
|
|
*tick_timer -= 1.0;
|
|
*ticks += 1;
|
|
new_tick_count = Some(*ticks);
|
|
}
|
|
} else {
|
|
panic!("clock state should be Started by now");
|
|
}
|
|
new_tick_count
|
|
}
|
|
}
|
|
|
|
impl Default for Clock {
|
|
fn default() -> Self {
|
|
Self::without_handle(Value::Fixed(ClockSpeed::TicksPerSecond(0.0)))
|
|
}
|
|
}
|
|
|
|
command_writers_and_readers! {
|
|
set_speed: ValueChangeCommand<ClockSpeed>,
|
|
set_ticking: bool,
|
|
reset: (),
|
|
}
|