all_is_cubes/sound/
mod.rs

1//! Defining sounds that are game content.
2//!
3//! All is Cubes (incompletely) supports two kinds of sounds:
4//!
5//! * [`SoundDef`]s, which define short sounds played in response to game events.
6//! * [`Ambient`], which define continuous sounds played based on a character's position in space.
7//!
8//! Currently, all sounds are synthesized based on a small set of parameters.
9//! In the future, short samples may be allowed.
10
11use core::fmt;
12
13use bevy_ecs::prelude as ecs;
14/// Acts as polyfill for float methods used in synthesis such as `sin()`
15#[cfg(not(feature = "std"))]
16#[allow(unused_imports)]
17use num_traits::float::Float as _;
18
19use crate::math::{PositiveSign, ZeroOne};
20use crate::transaction::{self, Equal, Transaction};
21use crate::universe;
22
23// -------------------------------------------------------------------------------------------------
24
25mod ambient;
26pub use ambient::{Ambient, Band, SpatialAmbient, Spectrum};
27
28// -------------------------------------------------------------------------------------------------
29
30/// A sound effect or grain.
31///
32/// [`SoundDef`]s are used as members of [`Universe`s][crate::universe::Universe]
33/// and may be referenced by ... TODO document
34#[derive(Clone, Debug, Eq, Hash, PartialEq, bevy_ecs::component::Component)]
35#[expect(clippy::module_name_repetitions)]
36#[non_exhaustive]
37pub struct SoundDef {
38    /// The total duration of the sound, in seconds. It may not be longer than one second.
39    pub duration: ZeroOne<f32>,
40
41    /// The frequency of the oscillator, in hertz.
42    pub frequency: PositiveSign<f32>,
43
44    /// TODO: Define amplitude scaling.
45    /// We probably need some physical reference because the obvious alternative, 0 dBFS,
46    /// is a bad idea. But some sounds are spatial and some are not...perhaps "1 meter/cube away
47    /// is the reference distance" will solve that.
48    pub amplitude: ZeroOne<f32>,
49    // TODO: More properties:
50    // * envelope attack
51    // * envelope decay *shape*
52    // * oscillator type(s)
53    //   * and an option to use a recording instead of an oscillator
54    // * modulation
55}
56
57impl SoundDef {
58    #[doc(hidden)] // API design still experimental
59    pub fn synthesize(&self, sample_rate: f32) -> impl Iterator<Item = [f32; 2]> {
60        let sample_count = (self.duration.into_inner() * sample_rate).round() as usize;
61        let sample_index_to_radians =
62            self.frequency.into_inner() * core::f32::consts::TAU / sample_rate;
63        let amplitude = self.amplitude.into_inner();
64
65        (0..sample_count).map(move |sample_index| {
66            let time_in_fraction = sample_index as f32 / (sample_count - 1).max(1) as f32;
67            let time_in_radians = sample_index as f32 * sample_index_to_radians;
68
69            let envelope: f32 = (1.0 - time_in_fraction).sqrt();
70
71            let wave: f32 = time_in_radians.sin() * amplitude;
72            [wave * envelope; 2]
73        })
74    }
75}
76
77universe::impl_universe_member_for_single_component_type!(SoundDef);
78
79impl universe::VisitHandles for SoundDef {
80    fn visit_handles(&self, _: &mut dyn universe::HandleVisitor) {
81        let Self {
82            duration: _,
83            frequency: _,
84            amplitude: _,
85        } = self;
86    }
87}
88
89// -------------------------------------------------------------------------------------------------
90
91/// A [`Transaction`] which replaces (or checks) a [`SoundDef`].
92#[derive(Clone, Debug, Eq, Hash, PartialEq)]
93pub struct DefTransaction {
94    old: Equal<SoundDef>,
95    new: Equal<SoundDef>,
96}
97
98impl transaction::Transactional for SoundDef {
99    type Transaction = DefTransaction;
100}
101
102impl DefTransaction {
103    /// Returns a transaction which fails if the current value of the [`SoundDef`] is not
104    /// equal to `old`.
105    pub fn expect(old: SoundDef) -> Self {
106        Self {
107            old: Equal(Some(old)),
108            new: Equal(None),
109        }
110    }
111
112    /// Returns a transaction which replaces the current value of the [`SoundDef`] with `new`.
113    pub fn overwrite(new: SoundDef) -> Self {
114        Self {
115            old: Equal(None),
116            new: Equal(Some(new)),
117        }
118    }
119
120    /// Returns a transaction which replaces the value of the [`SoundDef`] with `new`,
121    /// if it is equal to `old`, and otherwise fails.
122    pub fn replace(old: SoundDef, new: SoundDef) -> Self {
123        Self {
124            old: Equal(Some(old)),
125            new: Equal(Some(new)),
126        }
127    }
128}
129
130impl Transaction for DefTransaction {
131    type Target = SoundDef;
132    type CommitCheck = ();
133    // This ReadTicket is not currently used, but at least for now, *all* universe member transactions are to have ReadTicket as their context type.
134    type Context<'a> = universe::ReadTicket<'a>;
135    type Output = transaction::NoOutput;
136    type Mismatch = Mismatch;
137
138    fn check(
139        &self,
140        target: &SoundDef,
141        _read_ticket: universe::ReadTicket<'_>,
142    ) -> Result<Self::CommitCheck, Self::Mismatch> {
143        self.old.check(target).map_err(|_| Mismatch::Unexpected)
144    }
145
146    fn commit(
147        self,
148        target: &mut SoundDef,
149        (): Self::CommitCheck,
150        _outputs: &mut dyn FnMut(Self::Output),
151    ) -> Result<(), transaction::CommitError> {
152        if let Equal(Some(new)) = self.new {
153            *target = new;
154            // Note there is no change notification.
155            // It would be nice if we could arrange such notification to happen via the containing
156            // Handle instead of implementing it anew -- but for now, this should not be too bad
157            // except for editors.
158        }
159        Ok(())
160    }
161}
162
163impl universe::TransactionOnEcs for DefTransaction {
164    type WriteQueryData = &'static mut Self::Target;
165
166    fn check(
167        &self,
168        target: &SoundDef,
169        read_ticket: universe::ReadTicket<'_>,
170    ) -> Result<Self::CommitCheck, Self::Mismatch> {
171        Transaction::check(self, target, read_ticket)
172    }
173
174    fn commit(
175        self,
176        mut target: ecs::Mut<'_, SoundDef>,
177        check: Self::CommitCheck,
178    ) -> Result<(), transaction::CommitError> {
179        Transaction::commit(self, &mut *target, check, &mut transaction::no_outputs)
180    }
181}
182
183impl transaction::Merge for DefTransaction {
184    type MergeCheck = ();
185    type Conflict = Conflict;
186
187    fn check_merge(&self, other: &Self) -> Result<Self::MergeCheck, Self::Conflict> {
188        let conflict = Conflict {
189            old: self.old.check_merge(&other.old).is_err(),
190            new: self.new.check_merge(&other.new).is_err(),
191        };
192
193        if (conflict
194            != Conflict {
195                old: false,
196                new: false,
197            })
198        {
199            Err(conflict)
200        } else {
201            Ok(())
202        }
203    }
204
205    fn commit_merge(&mut self, other: Self, (): Self::MergeCheck) {
206        let Self { old, new } = self;
207        old.commit_merge(other.old, ());
208        new.commit_merge(other.new, ());
209    }
210}
211
212/// Transaction precondition error type for a [`DefTransaction`].
213#[derive(Clone, Debug, Eq, PartialEq, displaydoc::Display)]
214#[non_exhaustive]
215pub enum Mismatch {
216    /// old definition not as expected
217    Unexpected,
218}
219
220/// Transaction conflict error type for a [`DefTransaction`].
221// ---
222// TODO: this is identical to `BlockDefConflict` and `CubeConflict` but for the names
223#[derive(Clone, Copy, Debug, Eq, PartialEq)]
224#[non_exhaustive]
225pub struct Conflict {
226    /// The transactions have conflicting preconditions (`old` definitions).
227    pub(crate) old: bool,
228    /// The transactions are attempting to provide two different `new` definitions.
229    pub(crate) new: bool,
230}
231
232impl core::error::Error for Mismatch {}
233impl core::error::Error for Conflict {}
234
235impl fmt::Display for Conflict {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        match *self {
238            Conflict {
239                old: true,
240                new: false,
241            } => write!(f, "different preconditions for SoundDef"),
242            Conflict {
243                old: false,
244                new: true,
245            } => write!(f, "cannot write different new values to the same SoundDef"),
246            Conflict {
247                old: true,
248                new: true,
249            } => write!(f, "different preconditions (with write)"),
250            Conflict {
251                old: false,
252                new: false,
253            } => unreachable!(),
254        }
255    }
256}