aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKyle Simpson <[email protected]>2018-03-06 00:23:31 +0000
committerZeyla Hellyer <[email protected]>2018-03-05 16:23:31 -0800
commit9baf1675b0d1837fe010cfbadac8e80fd9d53898 (patch)
tree282c1b5e9291c8c10833fc5bad258412c7a4c09b /src
parentSupport sending files with an embed (#285) (diff)
downloadserenity-9baf1675b0d1837fe010cfbadac8e80fd9d53898.tar.xz
serenity-9baf1675b0d1837fe010cfbadac8e80fd9d53898.zip
Backport parts of 7d162b9
* Backport parts of 7d162b9. * Silent frame fixes. * Read-only playback position. * Opus Softclip for audio mixing. * Documentation for Audio structs. Not included (for now): * Bitrate control * Gutting/rework of Voice OpCodes, Heartbeats * Opus stream mixing * Minor adjustments due to manual edits.
Diffstat (limited to 'src')
-rw-r--r--src/voice/audio.rs84
-rw-r--r--src/voice/connection.rs25
-rw-r--r--src/voice/handler.rs3
-rw-r--r--src/voice/streamer.rs7
4 files changed, 109 insertions, 10 deletions
diff --git a/src/voice/audio.rs b/src/voice/audio.rs
index 8a001d1..cb60f57 100644
--- a/src/voice/audio.rs
+++ b/src/voice/audio.rs
@@ -1,5 +1,6 @@
use parking_lot::Mutex;
use std::sync::Arc;
+use std::time::Duration;
pub const HEADER_LEN: usize = 12;
pub const SAMPLE_RATE: u32 = 48_000;
@@ -36,12 +37,63 @@ pub enum AudioType {
/// Control object for audio playback.
///
/// Accessed by both commands and the playback code -- as such, access is
-/// always guarded.
+/// always guarded. In particular, you should expect to receive
+/// a [`LockedAudio`] when calling [`Handler::play_returning`] or
+/// [`Handler::play_only`].
+///
+/// # Example
+///
+/// ```rust,ignore
+/// use serenity::voice::{Handler, LockedAudio, ffmpeg};
+///
+/// let handler: Handler = /* ... */;
+/// let source = ffmpeg("../audio/my-favourite-song.mp3")?;
+/// let safe_audio: LockedAudio = handler.play_only();
+/// {
+/// let audio_lock = safe_audio_control.clone();
+/// let mut audio = audio_lock.lock();
+///
+/// audio.volume(0.5);
+/// }
+/// ```
+///
+/// [`LockedAudio`]: type.LockedAudio.html
+/// [`Handler::play_only`]: struct.Handler.html#method.play_only
+/// [`Handler::play_returning`]: struct.Handler.html#method.play_returning
pub struct Audio {
+
+ /// Whether or not this sound is currently playing.
+ ///
+ /// Can be controlled with [`play`] or [`pause`]
+ /// if chaining is desired.
+ ///
+ /// [`play`]: #method.play
+ /// [`pause`]: #method.pause
pub playing: bool,
+
+ /// The desired volume for playback.
+ ///
+ /// Sensible values fall between `0.0` and `1.0`.
+ /// Can be controlled with [`volume`] if chaining is desired.
+ ///
+ /// [`volume`]: #method.volume
pub volume: f32,
+
+ /// Whether or not the sound has finished, or reached the end of its stream.
+ ///
+ /// ***Read-only*** for now.
pub finished: bool,
+
+ /// Underlying data access object.
+ ///
+ /// *Calling code is not expected to use this.*
pub source: Box<AudioSource>,
+
+ /// The current position for playback.
+ ///
+ /// Consider the position fields **read-only** for now.
+ pub position: Duration,
+ pub position_modified: bool,
}
impl Audio {
@@ -51,26 +103,56 @@ impl Audio {
volume: 1.0,
finished: false,
source,
+ position: Duration::new(0, 0),
+ position_modified: false,
}
}
+ /// Sets [`playing`] to `true` in a manner that allows method chaining.
+ ///
+ /// [`playing`]: #structfield.playing
pub fn play(&mut self) -> &mut Self {
self.playing = true;
self
}
+ /// Sets [`playing`] to `false` in a manner that allows method chaining.
+ ///
+ /// [`playing`]: #structfield.playing
pub fn pause(&mut self) -> &mut Self {
self.playing = false;
self
}
+ /// Sets [`volume`] in a manner that allows method chaining.
+ ///
+ /// [`volume`]: #structfield.volume
pub fn volume(&mut self, volume: f32) -> &mut Self {
self.volume = volume;
self
}
+
+ /// Change the position in the stream for subsequent playback.
+ ///
+ /// Currently a No-op.
+ pub fn position(&mut self, position: Duration) -> &mut Self {
+ self.position = position;
+ self.position_modified = true;
+
+ self
+ }
+
+ /// Steps playback location forward by one frame.
+ ///
+ /// *Used internally*, although in future this might affect seek position.
+ pub(crate) fn step_frame(&mut self) {
+ self.position += Duration::from_millis(20);
+ self.position_modified = false;
+ }
+
}
/// Threadsafe form of an instance of the [`Audio`] struct, locked behind a
diff --git a/src/voice/connection.rs b/src/voice/connection.rs
index 3588818..a98dbc7 100644
--- a/src/voice/connection.rs
+++ b/src/voice/connection.rs
@@ -11,6 +11,7 @@ use opus::{
Channels,
Decoder as OpusDecoder,
Encoder as OpusEncoder,
+ SoftClip,
};
use parking_lot::Mutex;
use serde::Deserialize;
@@ -58,6 +59,7 @@ pub struct Connection {
key: Key,
sequence: u16,
silence_frames: u8,
+ soft_clip: SoftClip,
speaking: bool,
ssrc: u32,
thread_items: ThreadItems,
@@ -151,6 +153,8 @@ impl Connection {
let encoder = OpusEncoder::new(SAMPLE_RATE, Channels::Mono, CodingMode::Audio)?;
+ let soft_clip = SoftClip::new(Channels::Stereo);
+
// Per discord dev team's current recommendations:
// (https://discordapp.com/developers/docs/topics/voice-connections#heartbeating)
let temp_heartbeat = (hello.heartbeat_interval as f64 * 0.75) as u64;
@@ -159,17 +163,18 @@ impl Connection {
audio_timer: Timer::new(1000 * 60 * 4),
client: mutexed_client,
decoder_map: HashMap::new(),
- destination: destination,
- encoder: encoder,
+ destination,
+ encoder,
encoder_stereo: false,
- key: key,
+ key,
keepalive_timer: Timer::new(temp_heartbeat),
- udp: udp,
+ udp,
sequence: 0,
silence_frames: 0,
+ soft_clip,
speaking: false,
ssrc: hello.ssrc,
- thread_items: thread_items,
+ thread_items,
timestamp: 0,
user_id: info.user_id,
})
@@ -344,14 +349,20 @@ impl Connection {
}
aud.finished = finished;
+
+ if !finished {
+ aud.step_frame();
+ }
};
+ self.soft_clip.apply(&mut mix_buffer);
+
if len == 0 {
if self.silence_frames > 0 {
self.silence_frames -= 1;
// Explicit "Silence" frame.
- opus_frame.extend_from_slice(&[0xf, 0x8, 0xf, 0xf, 0xf, 0xe]);
+ opus_frame.extend_from_slice(&[0xf8, 0xff, 0xfe]);
} else {
// Per official guidelines, send 5x silence BEFORE we stop speaking.
self.set_speaking(false)?;
@@ -453,7 +464,7 @@ fn combine_audio(
let sample_index = if true_stereo { i } else { i/2 };
let sample = (raw_buffer[sample_index] as f32) / 32768.0;
- float_buffer[i] = (float_buffer[i] + sample*volume).max(-1.0).min(1.0);
+ float_buffer[i] = float_buffer[i] + sample * volume;
}
}
diff --git a/src/voice/handler.rs b/src/voice/handler.rs
index 82e117c..4124d66 100644
--- a/src/voice/handler.rs
+++ b/src/voice/handler.rs
@@ -257,10 +257,11 @@ impl Handler {
/// Plays audio from a source.
///
- /// Unlike `play`, this stops all other sources attached
+ /// Unlike [`play`] or [`play_returning`], this stops all other sources attached
/// to the channel.
///
/// [`play`]: #method.play
+ /// [`play_returning`]: #method.play_returning
pub fn play_only(&mut self, source: Box<AudioSource>) -> LockedAudio {
let player = Arc::new(Mutex::new(Audio::new(source)));
self.send(VoiceStatus::SetSender(Some(player.clone())));
diff --git a/src/voice/streamer.rs b/src/voice/streamer.rs
index bee23ed..c5954c8 100644
--- a/src/voice/streamer.rs
+++ b/src/voice/streamer.rs
@@ -154,7 +154,12 @@ pub fn dca<P: AsRef<OsStr>>(path: P) -> StdResult<Box<AudioSource>, DcaError> {
Ok(opus(metadata.is_stereo(), reader))
}
-/// Creates an Opus audio source.
+/// Creates an Opus audio source. This makes certain assumptions: namely, that the input stream
+/// is composed ONLY of opus frames of the variety that Discord expects.
+///
+/// If you want to decode a `.opus` file, use [`ffmpeg`]
+///
+/// [`ffmpeg`]: fn.ffmpeg.html
pub fn opus<R: Read + Send + 'static>(is_stereo: bool, reader: R) -> Box<AudioSource> {
Box::new(InputSource {
stereo: is_stereo,