diff options
| author | Kyle Simpson <[email protected]> | 2018-03-06 00:23:31 +0000 |
|---|---|---|
| committer | Zeyla Hellyer <[email protected]> | 2018-03-05 16:23:31 -0800 |
| commit | 9baf1675b0d1837fe010cfbadac8e80fd9d53898 (patch) | |
| tree | 282c1b5e9291c8c10833fc5bad258412c7a4c09b /src | |
| parent | Support sending files with an embed (#285) (diff) | |
| download | serenity-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.rs | 84 | ||||
| -rw-r--r-- | src/voice/connection.rs | 25 | ||||
| -rw-r--r-- | src/voice/handler.rs | 3 | ||||
| -rw-r--r-- | src/voice/streamer.rs | 7 |
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, |