Allow restricting audio playback to a custom region (#19400)

# Objective

Adds the ability to restrict playback of an audio source to a certain
region in time. In other words, you can set a custom start position and
duration for the audio clip. These options are set via the
`PlaybackSettings` component, and it works on all kinds of audio
sources.

## Solution

- Added public `start_position` and `duration` fields to
`PlaybackSettings`, both of type `Option<std::time::Duration>`.
- Used rodio's `Source::skip_duration` and `Source::take_duration`
functions to implement start position and duration, respectively.
- If the audio is looping, it interacts as you might expect - the loop
will start at the start position and end after the duration.
- If the start position is None (the default value), the audio will
start from the beginning, like normal. Similarly, if the duration is
None (default), the audio source will play for as long as possible.

## Testing

I tried adding a custom start position to all the existing audio
examples to test a bunch of different audio sources and settings, and
they all worked fine. I verified that it skips the right amount of time,
and that it skips the entire audio clip if the start position is longer
than the length of the clip. All my testing was done on Fedora Linux.

Update: I did similar testing for duration, and ensured that the two
options worked together in combination and interacted well with looping
audio.

---

## Showcase

```rust
// Play a 10 second segment of a song, starting at 0:30.5
commands.spawn((
    AudioPlayer::new(song_handle),
    PlaybackSettings::LOOP
        .with_start_position(Duration::from_secs_f32(30.5))
        .with_duration(Duration::from_secs(10))
));
```
This commit is contained in:
L. Borella (Villi) 2025-05-30 04:45:37 +10:00 committed by GitHub
parent fe678e1eeb
commit 59ef10562a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 104 additions and 6 deletions

View File

@ -57,6 +57,16 @@ pub struct PlaybackSettings {
/// Optional scale factor applied to the positions of this audio source and the listener,
/// overriding the default value configured on [`AudioPlugin::default_spatial_scale`](crate::AudioPlugin::default_spatial_scale).
pub spatial_scale: Option<SpatialScale>,
/// The point in time in the audio clip where playback should start. If set to `None`, it will
/// play from the beginning of the clip.
///
/// If the playback mode is set to `Loop`, each loop will start from this position.
pub start_position: Option<core::time::Duration>,
/// How long the audio should play before stopping. If set, the clip will play for at most
/// the specified duration. If set to `None`, it will play for as long as it can.
///
/// If the playback mode is set to `Loop`, each loop will last for this duration.
pub duration: Option<core::time::Duration>,
}
impl Default for PlaybackSettings {
@ -81,6 +91,8 @@ impl PlaybackSettings {
muted: false,
spatial: false,
spatial_scale: None,
start_position: None,
duration: None,
};
/// Will play the associated audio source in a loop.
@ -136,6 +148,18 @@ impl PlaybackSettings {
self.spatial_scale = Some(spatial_scale);
self
}
/// Helper to use a custom playback start position.
pub const fn with_start_position(mut self, start_position: core::time::Duration) -> Self {
self.start_position = Some(start_position);
self
}
/// Helper to use a custom playback duration.
pub const fn with_duration(mut self, duration: core::time::Duration) -> Self {
self.duration = Some(duration);
self
}
}
/// Settings for the listener for spatial audio sources.

View File

@ -156,12 +156,49 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
}
};
let decoder = audio_source.decoder();
match settings.mode {
PlaybackMode::Loop => sink.append(audio_source.decoder().repeat_infinite()),
PlaybackMode::Loop => match (settings.start_position, settings.duration) {
// custom start position and duration
(Some(start_position), Some(duration)) => sink.append(
decoder
.skip_duration(start_position)
.take_duration(duration)
.repeat_infinite(),
),
// custom start position
(Some(start_position), None) => {
sink.append(decoder.skip_duration(start_position).repeat_infinite());
}
// custom duration
(None, Some(duration)) => {
sink.append(decoder.take_duration(duration).repeat_infinite());
}
// full clip
(None, None) => sink.append(decoder.repeat_infinite()),
},
PlaybackMode::Once | PlaybackMode::Despawn | PlaybackMode::Remove => {
sink.append(audio_source.decoder());
match (settings.start_position, settings.duration) {
(Some(start_position), Some(duration)) => sink.append(
decoder
.skip_duration(start_position)
.take_duration(duration),
),
(Some(start_position), None) => {
sink.append(decoder.skip_duration(start_position));
}
(None, Some(duration)) => sink.append(decoder.take_duration(duration)),
(None, None) => sink.append(decoder),
}
}
};
}
let mut sink = SpatialAudioSink::new(sink);
@ -196,12 +233,49 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
}
};
let decoder = audio_source.decoder();
match settings.mode {
PlaybackMode::Loop => sink.append(audio_source.decoder().repeat_infinite()),
PlaybackMode::Loop => match (settings.start_position, settings.duration) {
// custom start position and duration
(Some(start_position), Some(duration)) => sink.append(
decoder
.skip_duration(start_position)
.take_duration(duration)
.repeat_infinite(),
),
// custom start position
(Some(start_position), None) => {
sink.append(decoder.skip_duration(start_position).repeat_infinite());
}
// custom duration
(None, Some(duration)) => {
sink.append(decoder.take_duration(duration).repeat_infinite());
}
// full clip
(None, None) => sink.append(decoder.repeat_infinite()),
},
PlaybackMode::Once | PlaybackMode::Despawn | PlaybackMode::Remove => {
sink.append(audio_source.decoder());
match (settings.start_position, settings.duration) {
(Some(start_position), Some(duration)) => sink.append(
decoder
.skip_duration(start_position)
.take_duration(duration),
),
(Some(start_position), None) => {
sink.append(decoder.skip_duration(start_position));
}
(None, Some(duration)) => sink.append(decoder.take_duration(duration)),
(None, None) => sink.append(decoder),
}
}
};
}
let mut sink = AudioSink::new(sink);