omg so much has changed idk
This commit is contained in:
1026
Cargo.lock
generated
1026
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,7 @@ opt-level = 1
|
||||
opt-level = 3
|
||||
|
||||
[workspace.dependencies]
|
||||
bevy = { version = "0.12", features = [
|
||||
bevy = { version = "0.12", default-features = false, features = [
|
||||
# "dynamic_linking",
|
||||
"multi-threaded",
|
||||
"trace",
|
||||
@@ -46,7 +46,7 @@ tracing = "0.1.40"
|
||||
|
||||
[dependencies]
|
||||
bevy.workspace = true
|
||||
clap.workspace = true
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
guardian_commands.workspace = true
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
[srs]
|
||||
server = "127.0.0.1:5002"
|
||||
server = "192.168.0.100:5003"
|
||||
|
||||
[grpc]
|
||||
url = "http://127.0.0.1:50051/"
|
||||
url = "http://192.168.0.100:50053/"
|
||||
poll_rate = 10
|
||||
|
||||
[stt]
|
||||
url = "http://192.168.0.2:3000/"
|
||||
|
||||
[channels.Overlord1-1]
|
||||
frequency = 251250000
|
||||
[channels.Magic1-1]
|
||||
frequency = 266250000
|
||||
modulation = "Am"
|
||||
voice = "David"
|
||||
|
||||
@@ -83,6 +83,35 @@ pub enum Squadron {
|
||||
Wild,
|
||||
Ninja,
|
||||
Jedi,
|
||||
Hawg,
|
||||
Boar,
|
||||
Pig,
|
||||
Tusk,
|
||||
ArmyAir,
|
||||
Apache,
|
||||
Crow,
|
||||
Chaos,
|
||||
Sioux,
|
||||
Gatling,
|
||||
Gunslinger,
|
||||
Hammerhead,
|
||||
Bootleg,
|
||||
Palehorse,
|
||||
Carnivore,
|
||||
Saber,
|
||||
Hornet,
|
||||
Squid,
|
||||
Ragin,
|
||||
Roman,
|
||||
Sting,
|
||||
Jury,
|
||||
Joker,
|
||||
Ram,
|
||||
Hawk,
|
||||
Devil,
|
||||
Check,
|
||||
Snake,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl ToString for Squadron {
|
||||
@@ -95,6 +124,7 @@ impl ToString for Squadron {
|
||||
Squadron::Dodge => "Dodge".to_string(),
|
||||
Squadron::Ford => "Ford".to_string(),
|
||||
Squadron::Chevy => "Chevy".to_string(),
|
||||
Squadron::Hornet => "Hornet".to_string(),
|
||||
Squadron::Pontiac => "Pontiac".to_string(),
|
||||
Squadron::Viper => "Viper".to_string(),
|
||||
Squadron::Venom => "Venom".to_string(),
|
||||
@@ -108,6 +138,34 @@ impl ToString for Squadron {
|
||||
Squadron::Wild => "Wild".to_string(),
|
||||
Squadron::Ninja => "Ninja".to_string(),
|
||||
Squadron::Jedi => "Jedi".to_string(),
|
||||
Squadron::Hawg => "Hawg".to_string(),
|
||||
Squadron::Boar => "Boar".to_string(),
|
||||
Squadron::Pig => "Pig".to_string(),
|
||||
Squadron::Tusk => "Tusk".to_string(),
|
||||
Squadron::ArmyAir => "ArmyAir".to_string(),
|
||||
Squadron::Apache => "Apache".to_string(),
|
||||
Squadron::Crow => "Crow".to_string(),
|
||||
Squadron::Chaos => "Chaos".to_string(),
|
||||
Squadron::Sioux => "Sioux".to_string(),
|
||||
Squadron::Gatling => "Gatling".to_string(),
|
||||
Squadron::Gunslinger => "Gunslinger".to_string(),
|
||||
Squadron::Hammerhead => "Hammerhead".to_string(),
|
||||
Squadron::Bootleg => "Bootleg".to_string(),
|
||||
Squadron::Palehorse => "Palehorse".to_string(),
|
||||
Squadron::Carnivore => "Carnivore".to_string(),
|
||||
Squadron::Saber => "Saber".to_string(),
|
||||
Squadron::Squid => "Squid".to_string(),
|
||||
Squadron::Ragin => "Ragin".to_string(),
|
||||
Squadron::Roman => "Roman".to_string(),
|
||||
Squadron::Sting => "Sting".to_string(),
|
||||
Squadron::Jury => "Jury".to_string(),
|
||||
Squadron::Joker => "Joker".to_string(),
|
||||
Squadron::Ram => "Ram".to_string(),
|
||||
Squadron::Hawk => "Hawk".to_string(),
|
||||
Squadron::Devil => "Devil".to_string(),
|
||||
Squadron::Check => "Check".to_string(),
|
||||
Squadron::Snake => "Snake".to_string(),
|
||||
Squadron::Custom(c) => c.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,6 +182,7 @@ impl std::str::FromStr for Squadron {
|
||||
"Dodge" => Ok(Squadron::Dodge),
|
||||
"Ford" => Ok(Squadron::Ford),
|
||||
"Chevy" => Ok(Squadron::Chevy),
|
||||
"Hornet" => Ok(Squadron::Hornet),
|
||||
"Pontiac" => Ok(Squadron::Pontiac),
|
||||
"Viper" => Ok(Squadron::Viper),
|
||||
"Venom" => Ok(Squadron::Venom),
|
||||
@@ -137,7 +196,34 @@ impl std::str::FromStr for Squadron {
|
||||
"Wild" => Ok(Squadron::Wild),
|
||||
"Ninja" => Ok(Squadron::Ninja),
|
||||
"Jedi" => Ok(Squadron::Jedi),
|
||||
_ => Err("Invalid squadron".into()),
|
||||
"Hawg" => Ok(Squadron::Hawg),
|
||||
"Boar" => Ok(Squadron::Boar),
|
||||
"Pig" => Ok(Squadron::Pig),
|
||||
"Tusk" => Ok(Squadron::Tusk),
|
||||
"ArmyAir" => Ok(Squadron::ArmyAir),
|
||||
"Apache" => Ok(Squadron::Apache),
|
||||
"Crow" => Ok(Squadron::Crow),
|
||||
"Chaos" => Ok(Squadron::Chaos),
|
||||
"Sioux" => Ok(Squadron::Sioux),
|
||||
"Gatling" => Ok(Squadron::Gatling),
|
||||
"Gunslinger" => Ok(Squadron::Gunslinger),
|
||||
"Hammerhead" => Ok(Squadron::Hammerhead),
|
||||
"Bootleg" => Ok(Squadron::Bootleg),
|
||||
"Palehorse" => Ok(Squadron::Palehorse),
|
||||
"Carnivore" => Ok(Squadron::Carnivore),
|
||||
"Saber" => Ok(Squadron::Saber),
|
||||
"Squid" => Ok(Squadron::Squid),
|
||||
"Ragin" => Ok(Squadron::Ragin),
|
||||
"Roman" => Ok(Squadron::Roman),
|
||||
"Sting" => Ok(Squadron::Sting),
|
||||
"Jury" => Ok(Squadron::Jury),
|
||||
"Joker" => Ok(Squadron::Joker),
|
||||
"Ram" => Ok(Squadron::Ram),
|
||||
"Hawk" => Ok(Squadron::Hawk),
|
||||
"Devil" => Ok(Squadron::Devil),
|
||||
"Check" => Ok(Squadron::Check),
|
||||
"Snake" => Ok(Squadron::Snake),
|
||||
c => Ok(Squadron::Custom(c.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,6 +311,7 @@ fn parse_element(input: &str) -> IResult<&str, Element> {
|
||||
"dodge" => Squadron::Dodge,
|
||||
"ford" => Squadron::Ford,
|
||||
"chevy" => Squadron::Chevy,
|
||||
"hornet" => Squadron::Hornet,
|
||||
"pontiac" => Squadron::Pontiac,
|
||||
"viper" => Squadron::Viper,
|
||||
"venom" => Squadron::Venom,
|
||||
@@ -238,13 +325,41 @@ fn parse_element(input: &str) -> IResult<&str, Element> {
|
||||
"wild" => Squadron::Wild,
|
||||
"ninja" => Squadron::Ninja,
|
||||
"jedi" => Squadron::Jedi,
|
||||
_ => {
|
||||
return Err(NomError::Error(nom::error::Error {
|
||||
input: squadron_str,
|
||||
code: ErrorKind::Alpha,
|
||||
}))
|
||||
}
|
||||
"hawg" => Squadron::Hawg,
|
||||
"boar" => Squadron::Boar,
|
||||
"pig" => Squadron::Pig,
|
||||
"tusk" => Squadron::Tusk,
|
||||
"armyair" => Squadron::ArmyAir,
|
||||
"apache" => Squadron::Apache,
|
||||
"crow" => Squadron::Crow,
|
||||
"chaos" => Squadron::Chaos,
|
||||
"sioux" => Squadron::Sioux,
|
||||
"gatling" => Squadron::Gatling,
|
||||
"gunslinger" => Squadron::Gunslinger,
|
||||
"hammerhead" => Squadron::Hammerhead,
|
||||
"bootleg" => Squadron::Bootleg,
|
||||
"palehorse" => Squadron::Palehorse,
|
||||
"carnivore" => Squadron::Carnivore,
|
||||
"saber" => Squadron::Saber,
|
||||
"squid" => Squadron::Squid,
|
||||
"ragin" => Squadron::Ragin,
|
||||
"roman" => Squadron::Roman,
|
||||
"sting" => Squadron::Sting,
|
||||
"jury" => Squadron::Jury,
|
||||
"joker" => Squadron::Joker,
|
||||
"ram" => Squadron::Ram,
|
||||
"hawk" => Squadron::Hawk,
|
||||
"devil" => Squadron::Devil,
|
||||
"check" => Squadron::Check,
|
||||
"snake" => Squadron::Snake,
|
||||
c => Squadron::Custom(c.to_string()),
|
||||
};
|
||||
// _ => {
|
||||
// return Err(NomError::Error(nom::error::Error {
|
||||
// input: squadron_str,
|
||||
// code: ErrorKind::Alpha,
|
||||
// }))
|
||||
// }
|
||||
|
||||
let group = group_str.unwrap_or((None, 0, None)).1;
|
||||
let unit = unit_str.unwrap_or((None, 0, None)).1;
|
||||
|
||||
@@ -8,7 +8,7 @@ use bevy::{
|
||||
system::{ResMut, Resource, SystemMeta, SystemParam},
|
||||
world::{unsafe_world_cell::UnsafeWorldCell, World},
|
||||
},
|
||||
log::{debug, warn},
|
||||
log::warn,
|
||||
};
|
||||
// use bevy::{input::keyboard::KeyboardInput, prelude::*};
|
||||
// use bevy_egui::egui::{self, Align, ScrollArea, TextEdit};
|
||||
@@ -118,8 +118,6 @@ unsafe impl<T: Command> SystemParam for ConsoleCommand<T> {
|
||||
let clap_command = T::command().no_binary_name(true);
|
||||
let arg_matches = clap_command.try_get_matches_from(command.args.iter());
|
||||
|
||||
debug!("Trying to parse as `{}`.", command.command_name);
|
||||
|
||||
match arg_matches {
|
||||
Ok(matches) => match T::from_arg_matches(&matches) {
|
||||
Ok(from_arg_matches) => {
|
||||
|
||||
@@ -35,11 +35,7 @@ thiserror = "1.0"
|
||||
symspell = "0.4.3"
|
||||
simsearch = "0.2"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.windows]
|
||||
version = "0.52"
|
||||
features = [
|
||||
"Foundation",
|
||||
"Foundation_Collections",
|
||||
"Storage_Streams",
|
||||
"Media_SpeechSynthesis",
|
||||
]
|
||||
hound = "3.5.1"
|
||||
dasp = { version = "0.11.0", features = ["all"] }
|
||||
urlencoding = "2.1.3"
|
||||
samplerate = "0.2.4"
|
||||
|
||||
@@ -14,13 +14,13 @@ use bevy::{
|
||||
};
|
||||
use crossbeam_channel::{unbounded, Receiver};
|
||||
use dcs_grpc::dcs::{
|
||||
common::v0::{Coalition, GroupCategory::Airplane, Unit},
|
||||
common::v0::{Coalition, GroupCategory, Unit},
|
||||
mission::v0::{
|
||||
mission_service_client::MissionServiceClient, stream_units_response::Update,
|
||||
StreamUnitsRequest,
|
||||
},
|
||||
};
|
||||
use tracing::{debug, info, warn};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Response {
|
||||
@@ -65,7 +65,7 @@ fn connect_to_grpc(mut commands: Commands, tokio: Res<TokioResource>, url: Res<G
|
||||
.stream_units(StreamUnitsRequest {
|
||||
poll_rate,
|
||||
max_backoff: Some(30),
|
||||
category: Airplane as i32,
|
||||
category: GroupCategory::Airplane as i32,
|
||||
})
|
||||
.await
|
||||
else {
|
||||
@@ -83,7 +83,7 @@ fn connect_to_grpc(mut commands: Commands, tokio: Res<TokioResource>, url: Res<G
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Error from gRPC: {:?}", e); // verbose or debug log
|
||||
error!("Error from gRPC: {:?}", e); // verbose or debug log
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,7 @@ fn connect_to_grpc(mut commands: Commands, tokio: Res<TokioResource>, url: Res<G
|
||||
|
||||
warn!("Disconnected from DCS: {}", url); // warn
|
||||
tx.send(Response::Disconnected).ok();
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -180,7 +181,22 @@ fn update_units(
|
||||
}
|
||||
|
||||
if let Some(playername) = &event.player_name {
|
||||
let playername = playername.clone();
|
||||
e.insert(Player::new(playername.clone()));
|
||||
|
||||
if let Some(index) = playername.find('|') {
|
||||
let custom_callsign = playername[..index].trim().to_string();
|
||||
|
||||
// testing callsign validity
|
||||
if guardian_commands::call::parse_call(&format!(
|
||||
"Overlord {} radio check",
|
||||
custom_callsign
|
||||
))
|
||||
.is_ok()
|
||||
{
|
||||
e.insert(Callsign::new(custom_callsign.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(group) = &event.group {
|
||||
|
||||
@@ -15,13 +15,16 @@ use bevy::{
|
||||
reflect::{std_traits::ReflectDefault, Reflect},
|
||||
utils::AHasher,
|
||||
};
|
||||
use dcs_grpc::dcs::{
|
||||
common::v0::Coalition,
|
||||
net::v0::{net_service_client::NetServiceClient, SendChatRequest},
|
||||
use dcs_grpc::dcs::trigger::v0::{
|
||||
trigger_service_client::TriggerServiceClient, OutTextForUnitRequest,
|
||||
};
|
||||
|
||||
use tracing::info;
|
||||
|
||||
use crate::{components::GrpcBaseUrl, TokioResource};
|
||||
use crate::{
|
||||
components::{GrpcBaseUrl, Id},
|
||||
TokioResource,
|
||||
};
|
||||
|
||||
pub struct TextPlugin;
|
||||
|
||||
@@ -33,36 +36,38 @@ impl Plugin for TextPlugin {
|
||||
|
||||
fn send_text_message(
|
||||
mut commands: Commands,
|
||||
query: Query<(Entity, &TextMessage), Added<TextMessage>>,
|
||||
query: Query<(Entity, &Id, &TextMessage), Added<TextMessage>>,
|
||||
url: Res<GrpcBaseUrl>,
|
||||
tokio: Res<TokioResource>,
|
||||
) {
|
||||
let url = url.to_string();
|
||||
for (ent, id, msg) in query.iter() {
|
||||
let url = url.to_string();
|
||||
|
||||
for (ent, msg) in query.iter() {
|
||||
if msg.as_str().is_empty() {
|
||||
commands.entity(ent).remove::<TextMessage>().despawn();
|
||||
commands.entity(ent).remove::<TextMessage>();
|
||||
continue;
|
||||
}
|
||||
|
||||
let message = msg.clone();
|
||||
let url = url.clone();
|
||||
let id: u32 = id.into();
|
||||
tokio.0.spawn(async move {
|
||||
let Ok(mut client) = NetServiceClient::connect(url.to_string()).await else {
|
||||
let Ok(mut client) = TriggerServiceClient::connect(url.to_string()).await else {
|
||||
return;
|
||||
};
|
||||
|
||||
let request = SendChatRequest {
|
||||
message: message.to_string(),
|
||||
coalition: Coalition::All as i32,
|
||||
// target_player_id: player_id,
|
||||
let request = OutTextForUnitRequest {
|
||||
text: message.to_string(),
|
||||
display_time: 15,
|
||||
clear_view: false,
|
||||
unit_id: id,
|
||||
};
|
||||
|
||||
info!("> {}", message);
|
||||
client.send_chat(request).await.ok();
|
||||
client.out_text_for_unit(request).await.ok();
|
||||
});
|
||||
|
||||
commands.entity(ent).remove::<TextMessage>().despawn();
|
||||
commands.entity(ent).remove::<TextMessage>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,8 @@ mod voice_codec;
|
||||
mod voice_command;
|
||||
|
||||
use guardian_commands::{call::parse_call, ConsoleCommandEntered, ConsoleConfiguration};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use simsearch::SimSearch;
|
||||
use tracing::{error, info, warn};
|
||||
use tracing::{debug, error, info, warn};
|
||||
pub use voice_command::VoiceCommand;
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
@@ -161,8 +160,8 @@ fn listen_srs(
|
||||
commands.entity(ent).insert(ClientHandler(client_handle));
|
||||
|
||||
let frequency = radio.frequency;
|
||||
let modulation = radio.modulation;
|
||||
let id = *id;
|
||||
let voice = radio.voice.clone();
|
||||
let stt_url = stt_url.as_str().to_string();
|
||||
radio.handle = Some(tokio.0.spawn(async move {
|
||||
loop {
|
||||
@@ -212,11 +211,7 @@ fn listen_srs(
|
||||
wav_audio_part: None,
|
||||
frequencies: vec![Frequency {
|
||||
freq: frequency as f64,
|
||||
modulation: if frequency <= 87_995_000 {
|
||||
voice_codec::Modulation::Fm
|
||||
} else {
|
||||
voice_codec::Modulation::Am
|
||||
},
|
||||
modulation: modulation.into(),
|
||||
encryption: Encryption::None,
|
||||
}],
|
||||
unit_id: id.into(),
|
||||
@@ -266,7 +261,7 @@ fn listen_srs(
|
||||
}
|
||||
}
|
||||
Some(data) = voice_handle.recv() => {
|
||||
frames.push(synthesize(data.as_str(), &voice).await?).await;
|
||||
frames.push(synthesize("192.168.0.238:10200", data.as_str()).await?).await;
|
||||
}
|
||||
Some(data) = voice_stream.next() => {
|
||||
match data {
|
||||
@@ -501,9 +496,12 @@ fn handle_voice_command(
|
||||
info!("Received: {}", raw);
|
||||
info!("Interpreted as: {}", cmd);
|
||||
|
||||
let Ok((_, call)) = parse_call(cmd) else {
|
||||
// no command, casual conversations?
|
||||
return;
|
||||
let call = match parse_call(cmd) {
|
||||
Ok((_, call)) => call,
|
||||
Err(e) => {
|
||||
debug!("Error parsing call: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let cmd = call.command;
|
||||
@@ -520,21 +518,6 @@ fn handle_voice_command(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Voice(String);
|
||||
|
||||
impl Voice {
|
||||
pub fn new(voice: impl Into<String>) -> Self {
|
||||
Self(voice.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Voice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct ClientHandler(Receiver<Client>);
|
||||
|
||||
@@ -542,7 +525,6 @@ pub struct ClientHandler(Receiver<Client>);
|
||||
pub struct Radio {
|
||||
pub frequency: u64, // the way srs wants it
|
||||
pub modulation: Modulation,
|
||||
pub voice: Voice,
|
||||
sguid: String,
|
||||
voice_sink: Option<Sender<VoiceMessage>>,
|
||||
message_sink: Option<Sender<MessageRequest>>,
|
||||
@@ -550,12 +532,11 @@ pub struct Radio {
|
||||
}
|
||||
|
||||
impl Radio {
|
||||
pub fn new(frequency: u64, modulation: impl Into<Modulation>, voice: Voice) -> Self {
|
||||
pub fn new(frequency: u64, modulation: impl Into<Modulation>) -> Self {
|
||||
let modulation = modulation.into();
|
||||
Self {
|
||||
frequency,
|
||||
modulation,
|
||||
voice,
|
||||
sguid: create_sguid(),
|
||||
voice_sink: None,
|
||||
message_sink: None,
|
||||
|
||||
@@ -7,6 +7,8 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use tokio_util::codec::{Decoder, Encoder, LengthDelimitedCodec};
|
||||
|
||||
use super::message;
|
||||
|
||||
pub const GUID_LENGTH: usize = 22;
|
||||
|
||||
pub const PACKET_HEADER_LENGTH: usize = size_of::<u16>() // UInt16 Packet Length - 2 bytes
|
||||
@@ -53,6 +55,20 @@ pub enum Modulation {
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl From<message::Modulation> for Modulation {
|
||||
fn from(value: message::Modulation) -> Self {
|
||||
match value {
|
||||
super::Modulation::Am => Modulation::Am,
|
||||
super::Modulation::Fm => Modulation::Fm,
|
||||
super::Modulation::Intercom => Modulation::Intercom,
|
||||
super::Modulation::Disabled => Modulation::Disabled,
|
||||
super::Modulation::HaveQuick => Modulation::Disabled,
|
||||
super::Modulation::Satcom => Modulation::Disabled,
|
||||
super::Modulation::Mids => Modulation::Disabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Encryption {
|
||||
None,
|
||||
|
||||
@@ -1,135 +1,155 @@
|
||||
// TODO: Look into SpeakNG to avoid the windows dependency https://crates.io/crates/espeakng
|
||||
use std::io::Cursor;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use bevy::log;
|
||||
use dasp::interpolate::sinc::Sinc;
|
||||
use dasp::ring_buffer;
|
||||
use dasp::signal;
|
||||
use dasp::Sample;
|
||||
use dasp::Signal;
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, error, warn};
|
||||
use windows::core::HSTRING;
|
||||
use windows::Media::SpeechSynthesis::SpeechSynthesizer;
|
||||
use windows::Storage::Streams::DataReader;
|
||||
use hound::WavSpec;
|
||||
use hound::WavWriter;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::net::ToSocketAddrs;
|
||||
|
||||
use crate::srs::Voice;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WinConfig {
|
||||
pub voice: Option<String>,
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Packet {
|
||||
#[serde(rename = "type")]
|
||||
pub r#type: String,
|
||||
pub version: String,
|
||||
pub data_length: Option<usize>,
|
||||
pub payload_length: Option<usize>,
|
||||
}
|
||||
|
||||
impl WinConfig {
|
||||
pub fn new(voice: &str) -> Self {
|
||||
Self {
|
||||
voice: Some(voice.to_string()),
|
||||
}
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
struct RequestData {
|
||||
text: String,
|
||||
}
|
||||
|
||||
static MUTEX: Mutex<()> = Mutex::const_new(());
|
||||
#[derive(Serialize)]
|
||||
struct Request {
|
||||
r#type: String,
|
||||
data: RequestData,
|
||||
}
|
||||
|
||||
pub async fn synthesize(text: &str, voice: &Voice) -> Result<Vec<Vec<u8>>, WinError> {
|
||||
let config = WinConfig::new(&voice.to_string());
|
||||
// Note, there does not seem to be a way to explicitly set 16000kHz, 16 audio bits per
|
||||
// sample and mono channel.
|
||||
pub async fn synthesize<A: ToSocketAddrs>(
|
||||
addr: A,
|
||||
text: &str,
|
||||
) -> Result<Vec<Vec<u8>>, Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
|
||||
// Prevent concurrent Windows TTS synthesis, as this might cause a crash.
|
||||
let lock = MUTEX.lock().await;
|
||||
let (mut read, mut write) = tokio::io::split(stream);
|
||||
|
||||
let mut voice_info = None;
|
||||
if let Some(voice) = &config.voice {
|
||||
let all_voices = SpeechSynthesizer::AllVoices()?;
|
||||
let len = all_voices.Size()? as usize;
|
||||
for i in 0..len {
|
||||
let v = all_voices.GetAt(i as u32)?;
|
||||
let lang = v.Language()?.to_string();
|
||||
if !lang.starts_with("en-") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = v.DisplayName()?.to_string();
|
||||
if name.ends_with(voice) {
|
||||
voice_info = Some(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// default to the first english voice in the list
|
||||
let all_voices = SpeechSynthesizer::AllVoices()?;
|
||||
let len = all_voices.Size()? as usize;
|
||||
for i in 0..len {
|
||||
let v = all_voices.GetAt(i as u32)?;
|
||||
let lang = v.Language()?.to_string();
|
||||
if lang.starts_with("en-") {
|
||||
let name = v.DisplayName()?.to_string();
|
||||
debug!("Using WIN voice: {}", name);
|
||||
voice_info = Some(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if voice_info.is_none() {
|
||||
error!("Could not find any english Windows TTS voice");
|
||||
}
|
||||
}
|
||||
|
||||
if voice_info.is_none() {
|
||||
let all_voices = SpeechSynthesizer::AllVoices()?;
|
||||
let len = all_voices.Size()? as usize;
|
||||
warn!(
|
||||
"Available WIN voices are (you don't have to include the `Microsoft` prefix in \
|
||||
the name):"
|
||||
);
|
||||
for i in 0..len {
|
||||
let v = all_voices.GetAt(i as u32)?;
|
||||
let lang = v.Language()?.to_string();
|
||||
if !lang.starts_with("en-") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = v.DisplayName()?.to_string();
|
||||
warn!("- {} ({})", name, lang);
|
||||
}
|
||||
}
|
||||
|
||||
let synth = SpeechSynthesizer::new()?;
|
||||
let lang = if let Some(info) = voice_info {
|
||||
synth.SetVoice(&info)?;
|
||||
info.Language()?.to_string().into()
|
||||
} else {
|
||||
Cow::Borrowed("en")
|
||||
let request = Request {
|
||||
r#type: "synthesize".to_string(),
|
||||
data: RequestData {
|
||||
text: text.to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
// the DataReader is !Send, which is why we have to process it in a local set
|
||||
let stream = synth
|
||||
.SynthesizeSsmlToStreamAsync(&HSTRING::from(&format!(
|
||||
r#"<speak version="1.0" xml:lang="{lang}">{text}</speak>"#
|
||||
)))?
|
||||
.await?;
|
||||
let size = stream.Size()?;
|
||||
let mut request = serde_json::to_string(&request)?;
|
||||
request.push('\n');
|
||||
|
||||
let rd = DataReader::CreateDataReader(&stream.GetInputStreamAt(0)?)?;
|
||||
rd.LoadAsync(size as u32)?.await?;
|
||||
write.write_all(request.as_bytes()).await?;
|
||||
|
||||
let mut wav = vec![0u8; size as usize];
|
||||
rd.ReadBytes(wav.as_mut_slice())?;
|
||||
let mut str = String::new();
|
||||
let mut pcm_buffer: Vec<u8> = Vec::new();
|
||||
loop {
|
||||
let Ok(c) = read.read_u8().await else {
|
||||
log::error!("Unable to read byte");
|
||||
break;
|
||||
};
|
||||
|
||||
drop(lock);
|
||||
if c == b'\n' {
|
||||
let Ok(packet) = serde_json::from_str::<Packet>(&str) else {
|
||||
log::error!("Unable to read packet from: {}", str);
|
||||
break;
|
||||
};
|
||||
str = String::new();
|
||||
|
||||
Ok(wav_to_opus(wav.into()).await?)
|
||||
}
|
||||
if let Some(length) = packet.data_length {
|
||||
let mut buf = (0..length).map(|_| 0u8).collect::<Vec<_>>();
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum WinError {
|
||||
#[error("Calling WinRT API failed with error code {0}: {1}")]
|
||||
Win(i32, String),
|
||||
#[error("Runtime error")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("failed to encode audio data as opus")]
|
||||
Opus(#[from] audiopus::Error),
|
||||
}
|
||||
if let Err(e) = read.read_exact(&mut buf).await {
|
||||
log::error!("Unable to read data: {:?}", e);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
impl From<windows::core::Error> for WinError {
|
||||
fn from(err: windows::core::Error) -> Self {
|
||||
WinError::Win(err.code().0, err.message().to_string())
|
||||
if let Some(length) = packet.payload_length {
|
||||
let mut buf = (0..length).map(|_| 0u8).collect::<Vec<_>>();
|
||||
|
||||
if let Err(e) = read.read_exact(&mut buf).await {
|
||||
log::error!("Unable to read data: {:?}", e);
|
||||
break;
|
||||
}
|
||||
|
||||
pcm_buffer.append(&mut buf);
|
||||
}
|
||||
|
||||
match packet.r#type.as_str() {
|
||||
"audio-start" => {
|
||||
pcm_buffer.clear();
|
||||
}
|
||||
"audio-stop" => {
|
||||
log::info!("Audio received, start post-processing");
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
str.push(c.into());
|
||||
}
|
||||
|
||||
let wav = to_wav(&pcm_buffer)?;
|
||||
|
||||
let Ok(reader) = hound::WavReader::new(&*wav) else {
|
||||
log::error!("Error opening reader");
|
||||
return Err("Error opening reader".into());
|
||||
};
|
||||
|
||||
// pcm_buffer is 22050Hz, need to convert this to 16000Hz
|
||||
let samples = reader
|
||||
.into_samples()
|
||||
.filter_map(Result::ok)
|
||||
.map(i16::to_sample::<f64>);
|
||||
|
||||
let signal = signal::from_interleaved_samples_iter(samples);
|
||||
|
||||
let ring_buffer = ring_buffer::Fixed::from([[0.0]; 100]);
|
||||
let sinc = Sinc::new(ring_buffer);
|
||||
|
||||
let new_signal = signal.from_hz_to_hz(sinc, 22050.0, 16000.0);
|
||||
|
||||
let spec = WavSpec {
|
||||
channels: 1,
|
||||
sample_rate: 16000,
|
||||
bits_per_sample: 16,
|
||||
sample_format: hound::SampleFormat::Int,
|
||||
};
|
||||
|
||||
let new_wav: Vec<u8> = Vec::new();
|
||||
let mut new_wav_cursor = Cursor::new(new_wav);
|
||||
|
||||
let mut writer = WavWriter::new(&mut new_wav_cursor, spec).unwrap();
|
||||
|
||||
for frame in new_signal.until_exhausted() {
|
||||
writer.write_sample(frame[0].to_sample::<i16>()).unwrap();
|
||||
}
|
||||
|
||||
writer.flush().unwrap();
|
||||
|
||||
drop(writer);
|
||||
|
||||
let wav = new_wav_cursor.into_inner();
|
||||
let wav = bytes::Bytes::copy_from_slice(&wav);
|
||||
|
||||
Ok(wav_to_opus(wav).await?)
|
||||
}
|
||||
|
||||
async fn wav_to_opus(wav: bytes::Bytes) -> Result<Vec<Vec<u8>>, audiopus::Error> {
|
||||
@@ -160,3 +180,30 @@ async fn wav_to_opus(wav: bytes::Bytes) -> Result<Vec<Vec<u8>>, audiopus::Error>
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn to_wav(data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
let input_buffer: Vec<u8> = Vec::new();
|
||||
let mut input_buffer_cursor = Cursor::new(input_buffer);
|
||||
|
||||
let input_spec = WavSpec {
|
||||
channels: 1,
|
||||
sample_rate: 22050,
|
||||
bits_per_sample: 16,
|
||||
sample_format: hound::SampleFormat::Int,
|
||||
};
|
||||
|
||||
let mut input = hound::WavWriter::new(&mut input_buffer_cursor, input_spec)?;
|
||||
|
||||
let audio_stream = data
|
||||
.chunks(2)
|
||||
.map(|bytes| i16::from_le_bytes(bytes.try_into().unwrap()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for frame in audio_stream {
|
||||
input.write_sample(frame.to_sample::<i16>()).unwrap();
|
||||
}
|
||||
|
||||
drop(input);
|
||||
|
||||
Ok(input_buffer_cursor.into_inner())
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "stable"
|
||||
|
||||
@@ -74,7 +74,7 @@ impl Braa {
|
||||
let bearing = split(&self.bearing.to_string(), 1);
|
||||
let range = split(&self.range.to_string(), 1);
|
||||
format!(
|
||||
"BRA <break time=\"250ms\"/> {} <break /> {} <break /> {} thousand <break /> {}",
|
||||
"BRA . . . {} . . . {} . . . {} thousand . . . {}",
|
||||
bearing, range, self.angels, self.aspect
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,13 +64,13 @@ pub fn bogey_dope(
|
||||
let (Some(n_position), Some(n_heading)) = n else {
|
||||
let message = format!("{}, {}, {}", p_callsign, a_callsign, "Skies are clear");
|
||||
let voice_message = format!(
|
||||
"{}, {}, {}",
|
||||
"{} . . . {} . . . {}",
|
||||
p_callsign.to_voice(),
|
||||
a_callsign.to_voice(),
|
||||
"Skies are clear"
|
||||
);
|
||||
|
||||
commands.spawn(TextMessage::new(message));
|
||||
commands.entity(*pilot).insert(TextMessage::new(message));
|
||||
commands
|
||||
.entity(*operator)
|
||||
.insert(VoiceMessage::new(voice_message));
|
||||
@@ -80,13 +80,13 @@ pub fn bogey_dope(
|
||||
let bra = Braa::new(p_pos, n_position, n_heading);
|
||||
let message = format!("{}, {}, {}", p_callsign, a_callsign, bra);
|
||||
let voice_message = format!(
|
||||
"{}, {}, {}",
|
||||
"{} . . . {} . . . {}",
|
||||
p_callsign.to_voice(),
|
||||
a_callsign.to_voice(),
|
||||
bra.to_voice()
|
||||
);
|
||||
|
||||
commands.spawn(TextMessage::new(message));
|
||||
commands.entity(*pilot).insert(TextMessage::new(message));
|
||||
commands
|
||||
.entity(*operator)
|
||||
.insert(VoiceMessage::new(voice_message));
|
||||
|
||||
@@ -53,7 +53,7 @@ pub fn clear_tripwire(
|
||||
a_callsign.to_voice(),
|
||||
);
|
||||
|
||||
commands.spawn(TextMessage::new(message));
|
||||
commands.entity(*pilot).insert(TextMessage::new(message));
|
||||
commands
|
||||
.entity(*operator)
|
||||
.insert(VoiceMessage::new(message_voice));
|
||||
|
||||
@@ -44,13 +44,13 @@ pub fn radio_check(
|
||||
|
||||
let message = format!("{}, {}, {}", p_callsign, a_callsign, "five by five");
|
||||
let voice_message = format!(
|
||||
"{}, {}, {}",
|
||||
"{} . . . {} . . . {}",
|
||||
p_callsign.to_voice(),
|
||||
a_callsign.to_voice(),
|
||||
"five by five"
|
||||
);
|
||||
|
||||
commands.spawn(TextMessage::new(message));
|
||||
commands.entity(*pilot).insert(TextMessage::new(message));
|
||||
commands
|
||||
.entity(*operator)
|
||||
.insert(VoiceMessage::new(voice_message));
|
||||
|
||||
@@ -96,7 +96,7 @@ pub fn set_tripwire(
|
||||
distance
|
||||
);
|
||||
|
||||
commands.spawn(TextMessage::new(message));
|
||||
commands.entity(*pilot).insert(TextMessage::new(message));
|
||||
commands
|
||||
.entity(*operator)
|
||||
.insert(VoiceMessage::new(message_voice));
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{collections::HashMap, path::PathBuf};
|
||||
use bevy::ecs::system::Resource;
|
||||
use guardian_core::{
|
||||
components::{GrpcBaseUrl, SrsSocketAddr, SttBaseUrl},
|
||||
srs::{Modulation as SrsModulation, Voice},
|
||||
srs::Modulation as SrsModulation,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -27,7 +27,6 @@ pub struct SrsConfig {
|
||||
pub struct ChannelConfig {
|
||||
pub frequency: u64,
|
||||
pub modulation: Modulation, // 0=AM, 1=FM?
|
||||
pub voice: Voice,
|
||||
}
|
||||
|
||||
impl TryFrom<PathBuf> for Config {
|
||||
@@ -57,7 +56,6 @@ impl Config {
|
||||
ChannelConfig {
|
||||
frequency: 251000000,
|
||||
modulation: Modulation::Am,
|
||||
voice: Voice::new("David"),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
44
src/main.rs
44
src/main.rs
@@ -3,6 +3,8 @@ mod commands;
|
||||
mod config;
|
||||
mod tripwire;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use bevy::{
|
||||
app::{App, Update},
|
||||
ecs::{
|
||||
@@ -11,6 +13,7 @@ use bevy::{
|
||||
system::{Commands, Query, Res},
|
||||
},
|
||||
};
|
||||
use clap::Parser;
|
||||
use commands::CommandsPlugin;
|
||||
use config::{Channels, Config};
|
||||
use guardian_core::{
|
||||
@@ -19,8 +22,15 @@ use guardian_core::{
|
||||
DefaultPlugins, TokioResource,
|
||||
};
|
||||
use tokio::runtime::Handle;
|
||||
use tracing::info;
|
||||
use tripwire::TripwirePlugin;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct CommandLineArguments {
|
||||
#[arg(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let subscriber = tracing_subscriber::FmtSubscriber::builder()
|
||||
@@ -35,7 +45,21 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber)?;
|
||||
|
||||
let config = Config::load("config.toml".into())?;
|
||||
let args = CommandLineArguments::parse();
|
||||
|
||||
let config = match args.config {
|
||||
Some(c) => {
|
||||
if !c.try_exists()? {
|
||||
return Err(format!("Config file {} not found", c.display()).into());
|
||||
}
|
||||
c
|
||||
}
|
||||
None => "config.toml".into(),
|
||||
};
|
||||
|
||||
info!("Loading config {}", config.display());
|
||||
|
||||
let config = Config::load(config)?;
|
||||
|
||||
App::new()
|
||||
.insert_resource(config.srs.server)
|
||||
@@ -64,20 +88,8 @@ fn give_awacs_radio(
|
||||
|
||||
bevy::log::info!("{} now has a radio", callsign);
|
||||
|
||||
commands.entity(ent).insert(Radio::new(
|
||||
channel.frequency,
|
||||
channel.modulation,
|
||||
channel.voice.clone(),
|
||||
));
|
||||
commands
|
||||
.entity(ent)
|
||||
.insert(Radio::new(channel.frequency, channel.modulation));
|
||||
}
|
||||
}
|
||||
|
||||
// // Initial player tripwire? get from some db so player doesnt need to redo it everytime
|
||||
// fn add_tripwire(mut commands: Commands, players: Query<Entity, Added<Player>>) {
|
||||
// for ent in players.iter() {
|
||||
// commands.entity(ent).insert(Tripwire {
|
||||
// range: 2.0,
|
||||
// reported: vec![],
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -26,7 +26,7 @@ impl Plugin for TripwirePlugin {
|
||||
fn tripwire(
|
||||
mut commands: Commands,
|
||||
awacs: Query<(Entity, &Callsign, &Radio), (With<Awacs>, With<Blue>, With<Radio>)>,
|
||||
mut players: Query<(&Callsign, &Position, &RadioInfo, &mut Tripwire), With<Player>>,
|
||||
mut players: Query<(Entity, &Callsign, &Position, &RadioInfo, &mut Tripwire), With<Player>>,
|
||||
npc: Query<(&Id, &Position, &Heading, &Velocity), (Without<Player>, With<Red>)>,
|
||||
) {
|
||||
// get the players radio frequencies
|
||||
@@ -34,7 +34,7 @@ fn tripwire(
|
||||
// broadcast message on that ferquency and as that awacs
|
||||
|
||||
for (ent, awacs, radio) in awacs.iter() {
|
||||
for (p_callsign, p_position, radioinfo, mut p_tripwire) in players.iter_mut() {
|
||||
for (p_ent, p_callsign, p_position, radioinfo, mut p_tripwire) in players.iter_mut() {
|
||||
// check if user is on the same frequency as awacs
|
||||
if !radioinfo
|
||||
.radios
|
||||
@@ -56,13 +56,14 @@ fn tripwire(
|
||||
let bra = Braa::new(p_position, n_position, n_heading);
|
||||
let message = format!("{}, {}, {}", p_callsign, awacs, bra);
|
||||
let voice_message = format!(
|
||||
"{}, {}, {}",
|
||||
"{} . . . {} . . . {}",
|
||||
p_callsign.to_voice(),
|
||||
awacs.to_voice(),
|
||||
bra.to_voice()
|
||||
);
|
||||
|
||||
commands.spawn(TextMessage::new(message));
|
||||
// commands.spawn((Id::new(player_id.into()), TextMessage::new(message)));
|
||||
commands.entity(p_ent).insert(TextMessage::new(message));
|
||||
commands
|
||||
.entity(ent)
|
||||
.insert(VoiceMessage::new(voice_message));
|
||||
|
||||
Reference in New Issue
Block a user