Discord: guild sounds decode, and a share can go into a guild voice channel
A guild's own soundboard sounds are Ogg Opus, which symphonia can unwrap but not decode; their packets now go to libopus, already here for calls. Found when the phone played one and moho logged 'unsupported feature'. startDiscordScreenShare / stopDiscordScreenShare named their channel by conversation only, and a guild's voice channel is no conversation - so a share in one never started. They now also take accountId and use the voice channel that account is in. Tested from moho into a guild voice channel and watched on a phone. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -254,6 +254,25 @@ pub fn decode(bytes: &[u8]) -> Result<Vec<i16>> {
|
||||
let track = format.default_track().context("the file has no audio in it")?;
|
||||
let track_id = track.id;
|
||||
let rate = track.codec_params.sample_rate.unwrap_or(48_000) as usize;
|
||||
// A guild's own sounds are Ogg Opus, which symphonia can unwrap but not
|
||||
// decode. libopus is here already for the calls themselves.
|
||||
if track.codec_params.codec == symphonia::core::codecs::CODEC_TYPE_OPUS {
|
||||
let mut opus = opus2::Decoder::new(48_000, opus2::Channels::Stereo).map_err(|e| anyhow!("no Opus decoder: {e:?}"))?;
|
||||
let mut out: Vec<i16> = Vec::new();
|
||||
let mut frame = vec![0i16; 5760 * 2];
|
||||
while let Ok(packet) = format.next_packet() {
|
||||
if packet.track_id() != track_id {
|
||||
continue;
|
||||
}
|
||||
if let Ok(n) = opus.decode(&packet.data, &mut frame, false) {
|
||||
out.extend_from_slice(&frame[..n * 2]);
|
||||
}
|
||||
if out.len() / 2 > 48_000 * LONGEST_SECS {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Ok(out);
|
||||
}
|
||||
let mut decoder = symphonia::default::get_codecs()
|
||||
.make(&track.codec_params, &DecoderOptions::default())
|
||||
.map_err(|e| anyhow!("no decoder for it: {e}"))?;
|
||||
@@ -302,6 +321,18 @@ fn resample(stereo: &[f32], from: usize, to: usize) -> Vec<i16> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// An Ogg Opus file - what a guild's own sounds are - decodes through
|
||||
/// libopus to 48kHz stereo.
|
||||
#[test]
|
||||
fn an_ogg_opus_sound_decodes() {
|
||||
let tone: Vec<f32> = (0..48_000).map(|n| (n as f32 * 0.05).sin() * 0.3).collect();
|
||||
let bytes = crate::oggopus::encode(&tone).unwrap();
|
||||
let pcm = decode(&bytes).unwrap();
|
||||
let secs = pcm.len() as f32 / 2.0 / 48_000.0;
|
||||
assert!((0.9..1.1).contains(&secs), "decoded {secs}s");
|
||||
assert!(pcm.iter().any(|s| s.saturating_abs() > 1000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sound_is_read_with_its_emoji() {
|
||||
let v = json!({ "sound_id": "1", "name": "quack", "volume": 0.5, "emoji_name": "🦆", "guild_id": "9", "available": true });
|
||||
|
||||
+19
-10
@@ -4709,17 +4709,26 @@ pub async fn dispatch(
|
||||
// stream; the connection it answers with is opened in the
|
||||
// background, and frames follow once it is up.
|
||||
"startDiscordScreenShare" | "stopDiscordScreenShare" => {
|
||||
let Some(buffer_id) = p_str_opt(params, "bufferId") else {
|
||||
return (None, Some(format!("{method} requires \"bufferId\"")));
|
||||
// Where the stream goes: the voice channel the account is in,
|
||||
// named by the account - which is the only way to name a guild's
|
||||
// voice channel, since it is no conversation and has no buffer -
|
||||
// or, as before, a DM's conversation.
|
||||
let (account_id, guild_id, channel_id) = if let Some(buffer_id) = p_str_opt(params, "bufferId") {
|
||||
let Some(buffer) = state.runtime.get_buffer(buffer_id) else {
|
||||
return (None, Some("no such buffer".to_string()));
|
||||
};
|
||||
let Some(channel_id) = state.runtime.get_discord_channel(buffer_id) else {
|
||||
return (None, Some("that conversation has no channel".to_string()));
|
||||
};
|
||||
(buffer.account_id.clone(), state.runtime.get_discord_guild(buffer_id), channel_id)
|
||||
} else if let Some(account_id) = p_str_opt(params, "accountId") {
|
||||
let Some((guild_id, channel_id)) = state.voice.current_channel(account_id) else {
|
||||
return (None, Some("not in a call".to_string()));
|
||||
};
|
||||
(account_id.to_string(), guild_id, channel_id)
|
||||
} else {
|
||||
return (None, Some(format!("{method} requires \"accountId\" or \"bufferId\"")));
|
||||
};
|
||||
let Some(buffer) = state.runtime.get_buffer(buffer_id) else {
|
||||
return (None, Some("no such buffer".to_string()));
|
||||
};
|
||||
let account_id = buffer.account_id.clone();
|
||||
let Some(channel_id) = state.runtime.get_discord_channel(buffer_id) else {
|
||||
return (None, Some("that conversation has no channel".to_string()));
|
||||
};
|
||||
let guild_id = state.runtime.get_discord_guild(buffer_id);
|
||||
let user_id = state.accounts.get_discord(&account_id).map(|a| a.user_id).unwrap_or_default();
|
||||
let key = backend::discord::golive::StreamKey {
|
||||
guild_id: guild_id.clone(),
|
||||
|
||||
Reference in New Issue
Block a user