Initial Commit

This commit is contained in:
AviiNL
2023-12-22 04:50:17 +01:00
parent 15f764aae8
commit f79b6b5dfb
41 changed files with 4379 additions and 270 deletions

View File

@@ -5,12 +5,40 @@ edition.workspace = true
repository.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lints]
workspace = true
[dependencies]
bevy.workspace = true
dcs-grpc.workspace = true
guardian_commands.workspace = true
serde.workspace = true
serde_json.workspace = true
shlex.workspace = true
tokio.workspace = true
async-compat = "0.2.1"
serde_repr = "0.1"
crossbeam-channel = "0.5.9"
tonic = "0.10"
tokio-util = { version = "0.7", features = ["codec", "net"] }
tokio-stream = { version = "0.1", features = ["sync"] }
futures-util = { version = "0.3", features = ["sink"] }
futures-core = { version = "0.3" }
base64 = "0.21.5"
byteorder = "1"
bytes = "1"
uuid = { version = "1.1", features = ["v4"] }
reqwest = { version = "0.11.22", features = ["blocking", "multipart"] }
audiopus = "0.3.0-rc.0"
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",
]

View File

@@ -0,0 +1,4 @@
use bevy::ecs::component::Component;
#[derive(Component)]
pub struct Awacs;

View File

@@ -0,0 +1,105 @@
use std::{
borrow::Cow,
hash::{Hash, Hasher},
};
use bevy::{
ecs::{component::Component, reflect::ReflectComponent},
reflect::{std_traits::ReflectDefault, Reflect},
utils::AHasher,
};
#[derive(Reflect, Component, Clone)]
#[reflect(Component, Default, Debug)]
pub struct Callsign {
hash: u64,
value: Cow<'static, str>,
}
impl Default for Callsign {
fn default() -> Self {
Callsign::new("")
}
}
fn add_spaces_around_numbers(input: &str) -> String {
let mut result = String::new();
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
result.push(c);
if let Some(next_char) = chars.peek() {
if next_char.is_numeric() {
result.push(' ');
}
}
}
result
}
impl Callsign {
/// Creates a new [`Callsign`] from any string-like type.
///
/// The internal hash will be computed immediately.
pub fn new(value: impl Into<Cow<'static, str>>) -> Self {
let value = value.into();
let mut value = Callsign { value, hash: 0 };
value.update_hash();
value
}
pub fn to_voice(&self) -> String {
let value: String = self.value.to_string();
let value = value.replace(' ', "");
let value = value.replace('-', "");
add_spaces_around_numbers(&value)
}
/// Sets the entity's value.
///
/// The internal hash will be re-computed.
#[inline(always)]
pub fn set(&mut self, value: impl Into<Cow<'static, str>>) {
*self = Callsign::new(value);
}
/// Updates the value of the entity in place.
///
/// This will allocate a new string if the value was previously
/// created from a borrow.
#[inline(always)]
pub fn mutate<F: FnOnce(&mut String)>(&mut self, f: F) {
f(self.value.to_mut());
self.update_hash();
}
/// Gets the value of the entity as a `&str`.
#[inline(always)]
pub fn as_str(&self) -> &str {
&self.value
}
fn update_hash(&mut self) {
let mut hasher = AHasher::default();
self.value.hash(&mut hasher);
self.hash = hasher.finish();
}
}
impl std::fmt::Display for Callsign {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(&self.value, f)
}
}
impl std::fmt::Debug for Callsign {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.value, f)
}
}

View File

@@ -0,0 +1,7 @@
use bevy::ecs::component::Component;
#[derive(Debug, Component)]
pub struct Group {
pub id: u32,
pub unit: u32,
}

View File

@@ -0,0 +1,4 @@
use bevy::ecs::component::Component;
#[derive(Debug, Component)]
pub struct Heading(pub f64);

View File

@@ -0,0 +1,22 @@
use bevy::ecs::component::Component;
#[derive(Debug, Component, Copy, Clone, PartialEq)]
pub struct Id(u32);
impl Id {
pub fn new(value: u32) -> Self {
Self(value)
}
}
impl From<&Id> for u32 {
fn from(value: &Id) -> Self {
value.0
}
}
impl From<Id> for u32 {
fn from(value: Id) -> Self {
value.0
}
}

View File

@@ -0,0 +1,23 @@
mod awacs;
mod callsign;
mod group;
mod grpc_base_url;
mod heading;
mod id;
mod player;
mod position;
mod side;
mod srs_socket_addr;
mod unit_type;
pub use awacs::*;
pub use callsign::*;
pub use group::*;
pub use grpc_base_url::*;
pub use heading::*;
pub use id::*;
pub use player::*;
pub use position::*;
pub use side::*;
pub use srs_socket_addr::*;
pub use unit_type::*;

View File

@@ -0,0 +1,79 @@
use std::{
borrow::Cow,
hash::{Hash, Hasher},
};
use bevy::{
ecs::{component::Component, reflect::ReflectComponent},
reflect::{std_traits::ReflectDefault, Reflect},
utils::AHasher,
};
#[derive(Reflect, Component, Clone)]
#[reflect(Component, Default, Debug)]
pub struct Player {
hash: u64,
value: Cow<'static, str>,
}
impl Default for Player {
fn default() -> Self {
Player::new("")
}
}
impl Player {
/// Creates a new [`Player`] from any string-like type.
///
/// The internal hash will be computed immediately.
pub fn new(value: impl Into<Cow<'static, str>>) -> Self {
let value = value.into();
let mut value = Player { value, hash: 0 };
value.update_hash();
value
}
/// Sets the entity's value.
///
/// The internal hash will be re-computed.
#[inline(always)]
pub fn set(&mut self, value: impl Into<Cow<'static, str>>) {
*self = Player::new(value);
}
/// Updates the value of the entity in place.
///
/// This will allocate a new string if the value was previously
/// created from a borrow.
#[inline(always)]
pub fn mutate<F: FnOnce(&mut String)>(&mut self, f: F) {
f(self.value.to_mut());
self.update_hash();
}
/// Gets the value of the entity as a `&str`.
#[inline(always)]
pub fn as_str(&self) -> &str {
&self.value
}
fn update_hash(&mut self) {
let mut hasher = AHasher::default();
self.value.hash(&mut hasher);
self.hash = hasher.finish();
}
}
impl std::fmt::Display for Player {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(&self.value, f)
}
}
impl std::fmt::Debug for Player {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.value, f)
}
}

View File

@@ -0,0 +1,49 @@
use bevy::ecs::component::Component;
#[derive(Debug, Component, Clone)]
pub struct Position {
pub lat: f64,
pub long: f64,
pub altitude: f64,
}
const COMPASS_DRIFT: i32 = -5;
impl Position {
pub fn distance_to_km(&self, other: &Position) -> f64 {
let radius_of_earth_in_km: f64 = 6371.0;
let delta_lat = (other.lat - self.lat).to_radians();
let delta_long = (other.long - self.long).to_radians();
let a = (delta_lat / 2.0).sin() * (delta_lat / 2.0).sin()
+ self.lat.to_radians().cos()
* other.lat.to_radians().cos()
* (delta_long / 2.0).sin()
* (delta_long / 2.0).sin();
let c = 2.0 * (a.sqrt().atan2((1.0 - a).sqrt()));
radius_of_earth_in_km * c
}
pub fn distance_to_nmi(&self, other: &Position) -> f64 {
self.distance_to_km(other) * 0.5399568035
}
pub fn get_bearing_to(&self, other: &Position) -> i32 {
let delta_long = (other.long - self.long).to_radians();
let y =
(other.long.to_radians() - self.long.to_radians()).sin() * other.lat.to_radians().cos();
let x = self.lat.to_radians().cos() * other.lat.to_radians().sin()
- self.lat.to_radians().sin() * other.lat.to_radians().cos() * delta_long.cos();
(((y.atan2(x).to_degrees() + 360.0) % 360.0) as i32) + COMPASS_DRIFT
}
pub fn feet(&self) -> f64 {
self.altitude * 3.28084
}
pub fn angels(&self) -> i32 {
(self.feet() / 1000.0).round() as i32
}
}

View File

@@ -0,0 +1,10 @@
use bevy::ecs::component::Component;
#[derive(Debug, Component)]
pub struct All;
#[derive(Debug, Component)]
pub struct Neutral;
#[derive(Debug, Component)]
pub struct Red;
#[derive(Debug, Component)]
pub struct Blue;

View File

@@ -0,0 +1,30 @@
use std::net::SocketAddr;
use bevy::ecs::system::Resource;
#[derive(Debug, Resource, Clone)]
pub struct SrsSocketAddr(SocketAddr);
impl Default for SrsSocketAddr {
fn default() -> Self {
Self("127.0.0.1:5002".parse().unwrap())
}
}
impl SrsSocketAddr {
pub fn new(addr: SocketAddr) -> Self {
Self(addr)
}
}
impl From<SocketAddr> for SrsSocketAddr {
fn from(value: SocketAddr) -> Self {
Self(value)
}
}
impl From<SrsSocketAddr> for SocketAddr {
fn from(value: SrsSocketAddr) -> Self {
value.0
}
}

View File

@@ -0,0 +1,79 @@
use std::{
borrow::Cow,
hash::{Hash, Hasher},
};
use bevy::{
ecs::{component::Component, reflect::ReflectComponent},
reflect::{std_traits::ReflectDefault, Reflect},
utils::AHasher,
};
#[derive(Reflect, Component, Clone)]
#[reflect(Component, Default, Debug)]
pub struct UnitType {
hash: u64,
value: Cow<'static, str>,
}
impl Default for UnitType {
fn default() -> Self {
UnitType::new("")
}
}
impl UnitType {
/// Creates a new [`UnitType`] from any string-like type.
///
/// The internal hash will be computed immediately.
pub fn new(value: impl Into<Cow<'static, str>>) -> Self {
let value = value.into();
let mut value = UnitType { value, hash: 0 };
value.update_hash();
value
}
/// Sets the entity's value.
///
/// The internal hash will be re-computed.
#[inline(always)]
pub fn set(&mut self, value: impl Into<Cow<'static, str>>) {
*self = UnitType::new(value);
}
/// Updates the value of the entity in place.
///
/// This will allocate a new string if the value was previously
/// created from a borrow.
#[inline(always)]
pub fn mutate<F: FnOnce(&mut String)>(&mut self, f: F) {
f(self.value.to_mut());
self.update_hash();
}
/// Gets the value of the entity as a `&str`.
#[inline(always)]
pub fn as_str(&self) -> &str {
&self.value
}
fn update_hash(&mut self) {
let mut hasher = AHasher::default();
self.value.hash(&mut hasher);
self.hash = hasher.finish();
}
}
impl std::fmt::Display for UnitType {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(&self.value, f)
}
}
impl std::fmt::Debug for UnitType {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.value, f)
}
}

View File

@@ -1,14 +1,15 @@
mod mission;
mod text;
mod voice;
pub mod mission;
pub mod text;
use crate::GrpcBaseUrl;
use crate::components::GrpcBaseUrl;
pub use self::mission::*;
pub use self::text::*;
use mission::MissionPlugin;
use text::TextPlugin;
// Re-export
pub use dcs_grpc::dcs::common::v0::Coalition;
use bevy::app::{App, Plugin, ScheduleRunnerPlugin};
pub use dcs_grpc::dcs::common::v0::{Coalition, Unit};
use std::time::Duration;
pub struct DcsPlugin;

View File

@@ -1,13 +1,14 @@
use std::time::Duration;
use crate::{GrpcBaseUrl, TokioResource};
use crate::{components::*, TokioResource};
use bevy::{
app::{App, Plugin, PreStartup, PreUpdate},
app::{App, Plugin, PostUpdate, PreStartup, PreUpdate},
core::Name,
ecs::{
component::Component,
entity::Entity,
event::{Event, EventReader, EventWriter},
query::With,
system::{Commands, Query, Res},
},
};
@@ -20,6 +21,12 @@ use dcs_grpc::dcs::{
},
};
#[derive(Clone)]
enum Response {
Disconnected,
Update(Box<Update>),
}
pub struct MissionPlugin;
impl Plugin for MissionPlugin {
@@ -31,7 +38,8 @@ impl Plugin for MissionPlugin {
app.add_systems(
PreUpdate,
(consume_stream_message, update_units, despawn_units),
);
)
.add_systems(PostUpdate, cleanup_after_disconnect);
}
}
@@ -60,16 +68,27 @@ fn connect_to_grpc(mut commands: Commands, tokio: Res<TokioResource>, url: Res<G
return;
};
let mut message = stream.get_mut().message().await;
while let Ok(Some(next)) = &message {
if let Some(update) = &next.update {
tx.send(update.clone()).ok();
loop {
match stream.get_mut().message().await {
Ok(Some(next)) => {
if let Some(update) = &next.update {
tx.send(Response::Update(Box::new(update.clone()))).ok();
}
}
Ok(None) => {
// eprintln!("Empty?");
break;
}
Err(e) => {
eprintln!("Error from gRPC: {:?}", e);
break;
}
}
message = stream.get_mut().message().await;
}
eprintln!("Disconnected");
tx.send(Response::Disconnected).ok();
tokio::time::sleep(Duration::from_secs(5)).await;
}
});
@@ -77,7 +96,23 @@ fn connect_to_grpc(mut commands: Commands, tokio: Res<TokioResource>, url: Res<G
commands.spawn(UnitsRequestMessage(task));
}
pub(crate) fn consume_stream_message(
fn cleanup_after_disconnect(
mut commands: Commands,
disconnected: Query<Entity, With<Disconnected>>,
entitiews: Query<Entity, With<Id>>,
) {
for d in disconnected.iter() {
// if there is one
commands.entity(d).despawn();
for ent in entitiews.iter() {
commands.entity(ent).despawn();
}
}
}
fn consume_stream_message(
mut commands: Commands,
requests: Query<&UnitsRequestMessage>,
mut ev_unit_updated: EventWriter<UnitUpdatedEvent>,
mut ev_unit_gone: EventWriter<UnitGoneEvent>,
@@ -85,18 +120,23 @@ pub(crate) fn consume_stream_message(
for stream in requests.iter() {
if let Ok(update) = stream.0.try_recv() {
match update {
Update::Gone(unit) => {
ev_unit_gone.send(UnitGoneEvent(unit.id));
}
Update::Unit(unit) => {
ev_unit_updated.send(UnitUpdatedEvent(unit));
Response::Update(boxed) => match *boxed {
Update::Gone(unit) => {
ev_unit_gone.send(UnitGoneEvent(unit.id));
}
Update::Unit(unit) => {
ev_unit_updated.send(UnitUpdatedEvent(unit));
}
},
Response::Disconnected => {
commands.spawn(Disconnected);
}
}
}
}
}
pub(crate) fn update_units(
fn update_units(
mut commands: Commands,
units: Query<(Entity, &Id)>,
mut ev: EventReader<UnitUpdatedEvent>,
@@ -106,14 +146,14 @@ pub(crate) fn update_units(
let mut e: Option<_> = None;
for (ent, id) in units.iter() {
if id.0 == event.id {
if &Id::new(event.id) == id {
e = commands.get_entity(ent);
break;
}
}
if e.is_none() {
e = Some(commands.spawn((Id(event.id), Callsign(event.callsign.clone()))));
e = Some(commands.spawn((Id::new(event.id), Callsign::new(event.callsign.clone()))));
}
let Some(mut e) = e else {
@@ -133,7 +173,7 @@ pub(crate) fn update_units(
}
if let Some(playername) = &event.player_name {
e.insert(Player(playername.clone()));
e.insert(Player::new(playername.clone()));
}
if let Some(group) = &event.group {
@@ -145,23 +185,36 @@ pub(crate) fn update_units(
e.insert(Name::new(event.name.clone()));
e.insert(UnitType(event.r#type.clone()));
if ["A-50", "E-3A", "E-2C", "KJ-2000"].contains(&event.r#type.as_str()) {
e.insert(Awacs);
}
e.insert(Side(
Coalition::try_from(event.coalition).expect("Coalition to be correct"),
));
e.insert(UnitType::new(event.r#type.clone()));
{
e.remove::<All>();
e.remove::<Neutral>();
e.remove::<Red>();
e.remove::<Blue>();
match Coalition::try_from(event.coalition).expect("Coalition to be correct") {
Coalition::All => e.insert(All),
Coalition::Neutral => e.insert(Neutral),
Coalition::Red => e.insert(Red),
Coalition::Blue => e.insert(Blue),
};
}
}
}
pub(crate) fn despawn_units(
fn despawn_units(
mut commands: Commands,
units: Query<(Entity, &Id)>,
mut ev: EventReader<UnitGoneEvent>,
) {
for event in ev.read() {
let gid = &event.0;
let gid = event.0;
for (ent, id) in units.iter() {
if gid == &id.0 {
if &Id::new(gid) == id {
commands.entity(ent).despawn();
}
}
@@ -169,83 +222,13 @@ pub(crate) fn despawn_units(
}
#[derive(Event)]
pub(crate) struct UnitUpdatedEvent(Unit);
struct UnitUpdatedEvent(Unit);
#[derive(Event)]
pub(crate) struct UnitGoneEvent(u32);
struct UnitGoneEvent(u32);
#[derive(Component)]
pub(crate) struct UnitsRequestMessage(Receiver<Update>);
struct UnitsRequestMessage(Receiver<Response>);
#[derive(Debug, Component)]
pub struct Id(pub u32);
#[derive(Debug, Component)]
pub struct Callsign(pub String);
#[derive(Debug, Component)]
pub struct UnitType(pub String);
#[derive(Debug, Component)]
pub struct Player(pub String);
#[derive(Debug, Component)]
pub struct Side(pub Coalition);
#[derive(Debug, Component)]
pub struct Position {
pub lat: f64,
pub long: f64,
pub altitude: f64,
}
#[derive(Debug, Component)]
pub struct Heading(pub f64);
#[derive(Debug, Component)]
pub struct Group {
pub id: u32,
pub unit: u32,
}
const COMPASS_DRIFT: i32 = -5;
impl Position {
pub fn distance_to_km(&self, other: &Position) -> f64 {
let radius_of_earth_in_km: f64 = 6371.0;
let delta_lat = (other.lat - self.lat).to_radians();
let delta_long = (other.long - self.long).to_radians();
let a = (delta_lat / 2.0).sin() * (delta_lat / 2.0).sin()
+ self.lat.to_radians().cos()
* other.lat.to_radians().cos()
* (delta_long / 2.0).sin()
* (delta_long / 2.0).sin();
let c = 2.0 * (a.sqrt().atan2((1.0 - a).sqrt()));
radius_of_earth_in_km * c
}
pub fn distance_to_nmi(&self, other: &Position) -> f64 {
self.distance_to_km(other) * 0.5399568035
}
pub fn get_bearing_to(&self, other: &Position) -> i32 {
let delta_long = (other.long - self.long).to_radians();
let y =
(other.long.to_radians() - self.long.to_radians()).sin() * other.lat.to_radians().cos();
let x = self.lat.to_radians().cos() * other.lat.to_radians().sin()
- self.lat.to_radians().sin() * other.lat.to_radians().cos() * delta_long.cos();
(((y.atan2(x).to_degrees() + 360.0) % 360.0) as i32) + COMPASS_DRIFT
}
pub fn feet(&self) -> f64 {
self.altitude * 3.28084
}
pub fn angels(&self) -> i32 {
(self.feet() / 1000.0).round() as i32
}
}
#[derive(Component)]
struct Disconnected;

View File

@@ -20,7 +20,7 @@ use dcs_grpc::dcs::{
net::v0::{net_service_client::NetServiceClient, SendChatRequest},
};
use crate::{GrpcBaseUrl, TokioResource};
use crate::{components::GrpcBaseUrl, TokioResource};
pub struct TextPlugin;

View File

@@ -1,12 +1,15 @@
mod dcs;
mod grpc_base_url;
pub mod components;
pub mod dcs;
pub mod srs;
mod tts;
pub use dcs::*;
pub use grpc_base_url::*;
use dcs::DcsPlugin;
use srs::SrsPlugin;
use bevy::app::PluginGroup;
use bevy::app::PluginGroupBuilder;
use bevy::ecs::system::Resource;
use bevy::{
app::{PluginGroup, PluginGroupBuilder},
ecs::system::Resource,
};
use tokio::runtime::Handle;
pub struct DefaultPlugins;
@@ -14,7 +17,9 @@ pub struct DefaultPlugins;
impl PluginGroup for DefaultPlugins {
fn build(self) -> PluginGroupBuilder {
#[allow(unused_mut)]
let mut group = PluginGroupBuilder::start::<Self>().add(DcsPlugin);
let mut group = PluginGroupBuilder::start::<Self>()
.add(DcsPlugin)
.add(SrsPlugin);
group
}

View File

@@ -0,0 +1,559 @@
pub mod frame_queue;
mod message;
mod messages_codec;
pub mod voice;
mod voice_codec;
mod voice_command;
use guardian_commands::{call::parse_call, ConsoleCommandEntered, ConsoleConfiguration};
use simsearch::SimSearch;
pub use voice_command::VoiceCommand;
use crossbeam_channel::Receiver;
pub use message::{Modulation, RadioInfo};
use std::{
collections::{hash_map::Entry, HashMap},
net::SocketAddr,
sync::Arc,
time::Duration,
};
const SRS_VERSION: &str = "1.9.0.0";
use bevy::{
app::{Plugin, PostUpdate, Update},
ecs::{
component::Component,
entity::Entity,
event::EventWriter,
query::{Added, Changed, With},
system::{Commands, Query, Res},
},
};
use futures_util::{SinkExt, StreamExt};
use tokio::{
net::{TcpStream, UdpSocket},
sync::{mpsc::Sender, RwLock},
task::JoinHandle,
time::sleep,
};
use tokio_util::{
codec::{FramedRead, FramedWrite},
udp::UdpFramed,
};
use crate::{
components::*,
srs::{
message::{Message, SyncMessage, VersionMismatchMessage},
voice_codec::{Encryption, Frequency},
},
tts::synthesize,
TokioResource,
};
use self::{
frame_queue::FrameQueue,
message::{
create_sguid, Client, Coalition, MessageRequest, MsgType, RadioUpdateMessage,
SyncMessageRequest, UpdateMessage,
},
messages_codec::MessagesCodec,
voice::VoiceMessage,
voice_codec::{VoiceCodec, VoicePacket},
};
pub struct SrsPlugin;
#[derive(Component)]
struct MessageComponent(Receiver<ReceivedMessage>);
#[derive(Debug, Clone)]
pub struct ReceivedMessage {
pub unit_sguid: String,
pub message: String,
}
impl Plugin for SrsPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.world
.get_resource_or_insert_with(SrsSocketAddr::default);
app.add_systems(Update, (listen_srs, update_srs_position, consume_message));
app.add_systems(
PostUpdate,
(transmit_message, update_client_radio, handle_voice_command),
);
}
}
fn listen_srs(
mut commands: Commands,
mut units: Query<
(
Entity,
&Id,
&Callsign,
&Position,
&mut Radio,
Option<&Red>,
Option<&Blue>,
),
Added<Radio>,
>,
tokio: Res<TokioResource>,
addr: Res<SrsSocketAddr>,
) {
for (ent, id, callsign, position, mut radio, red, blue) in units.iter_mut() {
let addr: SocketAddr = addr.clone().into();
let callsign = callsign.as_str().to_string();
let coalition = if red.is_some() {
Coalition::Red
} else if blue.is_some() {
Coalition::Blue
} else {
continue; // no coalition, no comms
};
let (tx, task) = crossbeam_channel::unbounded();
commands.entity(ent).insert(MessageComponent(task));
let client = Client {
client_guid: radio.sguid.clone(),
name: callsign.clone(),
seat: 0,
coalition,
allow_record: false,
radio_info: Some(RadioInfo {
radios: vec![message::Radio {
enc: false,
enc_key: 1,
freq: radio.frequency as f64,
modulation: radio.modulation,
sec_freq: 1.0,
retransmit: false,
}],
unit: callsign.clone(),
unit_id: id.into(),
iff: Default::default(),
}),
lat_lng_position: message::Position {
lat: position.lat,
lon: position.long,
alt: position.altitude,
},
};
let mut sguid = [0; 22];
sguid.clone_from_slice(radio.sguid.as_bytes());
let (voice_sink, mut voice_handle) = tokio::sync::mpsc::channel(128);
let (message_sink, mut message_handle) = tokio::sync::mpsc::channel(128);
let (client_sink, client_handle) = crossbeam_channel::unbounded();
radio.voice_sink = Some(voice_sink);
radio.message_sink = Some(message_sink);
commands.entity(ent).insert(ClientHandler(client_handle));
let frequency = radio.frequency;
let id = *id;
radio.handle = Some(tokio.0.spawn(async move {
let tcp = TcpStream::connect(addr).await?;
let (tcp_stream, tcp_sink) = tcp.into_split();
let mut messages_sink = FramedWrite::new(tcp_sink, MessagesCodec::new());
let mut messages_stream = FramedRead::new(tcp_stream, MessagesCodec::new());
let udp = UdpSocket::bind(SocketAddr::from(([0, 0, 0, 0], 0))).await?;
udp.connect(addr).await?;
let mut voice_ping_interval = tokio::time::interval(Duration::from_secs(15));
let (mut voice_sink, mut voice_stream) = UdpFramed::new(udp, VoiceCodec::new()).split();
let mut packet_id = 1;
messages_sink
.send(MessageRequest::Sync(SyncMessageRequest {
msg_type: MsgType,
client,
version: SRS_VERSION.to_string(),
}))
.await?;
let transmissions = Arc::new(RwLock::new(HashMap::<u32, Vec<VoicePacket>>::new()));
let frames: FrameQueue<Vec<Vec<u8>>> = FrameQueue::new();
loop {
tokio::select! {
Some(data) = frames.next() => {
let start = tokio::time::Instant::now();
for (i, frame) in data.into_iter().enumerate() {
if frame.is_empty() {
continue;
}
let packet = VoicePacket {
audio_part: frame,
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
},
encryption: Encryption::None,
}],
unit_id: id.into(),
packet_id,
hop_count: 0,
transmission_sguid: sguid,
client_sguid: sguid,
};
voice_sink.send((packet.into(), addr)).await.ok();
packet_id = packet_id.wrapping_add(1);
let playtime = Duration::from_millis((i as u64 + 1) * 20); // 20m per frame count
let elapsed = start.elapsed();
if playtime > elapsed {
let s = playtime - elapsed;
sleep(s).await;
}
}
}
Some(data) = message_handle.recv() => {
messages_sink.send(data).await?;
}
Some(Ok(data)) = messages_stream.next() => {
match &data {
Message::VersionMismatch(VersionMismatchMessage { version, .. }) => {
eprintln!("Version mismatch {} != {}", SRS_VERSION, version);
},
Message::Sync(SyncMessage { clients, .. }) => {
for client in clients.iter() {
client_sink.send(client.clone()).ok();
}
}
Message::RadioUpdate(RadioUpdateMessage { client, .. })=> {
client_sink.send(client.clone()).ok();
}
_ => {},
}
}
Some(data) = voice_handle.recv() => {
frames.push(synthesize(data.as_str()).await?).await;
}
Some(Ok(data)) = voice_stream.next() => {
// Collect voice packets
let (data, _) = data;
let unit_id = data.unit_id;
let mut t = transmissions.write().await;
if let Entry::Vacant(e) = t.entry(unit_id)
{
e.insert(vec![data]);
} else {
let t1 = t.get_mut(&unit_id).unwrap();
t1.push(data);
}
}
_ = tokio::time::sleep(Duration::from_millis(200)) => {
// Process voice packets
if transmissions.read().await.is_empty() {
continue;
}
let mut transmissions = transmissions.write().await;
for (_, data) in transmissions.iter() {
let mut wav_data = vec![];
let mut unit_sguid = String::new();
for d in data {
unit_sguid = String::from_utf8_lossy(&d.client_sguid).to_string();
if let Some(wav) = &d.wav_audio_part {
wav_data.extend_from_slice(wav);
}
}
let Some(message) = whisper("http://192.168.0.2:3000/", &wav_data).await else {
continue;
};
tx.send(ReceivedMessage {
unit_sguid,
message
}).ok();
}
transmissions.clear();
}
_ = voice_ping_interval.tick() => {
voice_sink.send((voice_codec::Packet::Ping(sguid), addr)).await?;
}
};
}
#[allow(unreachable_code)]
Ok(())
}));
}
}
fn consume_message(
mut commands: Commands,
messages: Query<(Entity, &MessageComponent)>,
units: Query<(Entity, &Sguid)>,
) {
for (entity, message) in messages.iter() {
if let Ok(message) = message.0.try_recv() {
for (ent, sguid) in units.iter() {
if sguid.0 == message.unit_sguid {
commands
.entity(ent)
.insert(VoiceCommand::new(message.message.clone(), entity));
break;
}
}
}
}
}
fn transmit_message(
mut commands: Commands,
messages: Query<(Entity, &Radio, &VoiceMessage), Added<VoiceMessage>>,
) {
for (ent, radio, msg) in messages.iter() {
if let Some(sink) = &radio.voice_sink {
sink.blocking_send(msg.clone()).ok();
commands.entity(ent).remove::<VoiceMessage>();
}
}
}
#[derive(Component)]
struct Sguid(String);
fn update_client_radio(
mut commands: Commands,
players: Query<(Entity, &Player), With<Player>>,
clients: Query<&ClientHandler>,
) {
let Some(client) = clients.iter().next() else {
return; // there is no client handler, and if there is, we only need one
};
if let Ok(client) = client.0.try_recv() {
for (ent, player) in players.iter() {
if player.as_str() == client.name {
commands
.entity(ent)
.insert(Sguid(client.client_guid.clone()));
if let Some(radio_info) = &client.radio_info {
commands.entity(ent).insert(radio_info.clone());
}
}
}
}
}
fn update_srs_position(
units: Query<
(
&Id,
&Callsign,
&Position,
&Radio,
Option<&Red>,
Option<&Blue>,
),
Changed<Position>,
>,
) {
for (id, callsign, position, radio, red, blue) in units.iter() {
let Some(message) = &radio.message_sink else {
continue; // no sink available
};
let coalition = if red.is_some() {
Coalition::Red
} else if blue.is_some() {
Coalition::Blue
} else {
continue; // no coalition, no comms
};
let callsign = callsign.as_str().to_string();
let client = Client {
client_guid: radio.sguid.clone(),
name: callsign.clone(),
seat: 0,
coalition,
allow_record: false,
radio_info: Some(RadioInfo {
radios: vec![message::Radio {
enc: false,
enc_key: 1,
freq: radio.frequency as f64,
modulation: radio.modulation,
sec_freq: 1.0,
retransmit: false,
}],
unit: callsign.clone(),
unit_id: id.into(),
iff: Default::default(),
}),
lat_lng_position: message::Position {
lat: position.lat,
lon: position.long,
alt: position.altitude,
},
};
message
.blocking_send(MessageRequest::Update(UpdateMessage {
msg_type: MsgType,
client: client.clone(),
version: SRS_VERSION.to_string(),
}))
.ok();
message
.blocking_send(MessageRequest::RadioUpdate(RadioUpdateMessage {
msg_type: MsgType,
client: client.clone(),
version: SRS_VERSION.to_string(),
}))
.ok();
}
}
fn handle_voice_command(
awacs: Query<(Entity, &Callsign)>,
cmds: Query<(Entity, &Callsign, &VoiceCommand), Changed<VoiceCommand>>,
config: Res<ConsoleConfiguration>,
mut command_entered: EventWriter<ConsoleCommandEntered>,
) {
for (ent, callsign, voice_command) in cmds.iter() {
let Ok((awacs_ent, awacs)) = awacs.get(*voice_command.get_entity()) else {
continue;
};
let all_commands = config.commands.keys().collect::<Vec<_>>();
let rawr = voice_command.as_str().to_lowercase();
let rawr = rawr.replace(&['(', ')', ',', '\"', '.', ';', ':', '\'', '?'][..], " ");
if rawr.is_empty() {
continue;
}
let mut engine: SimSearch<String> = SimSearch::new();
for cmd in &all_commands {
let awacs = awacs.to_voice();
let awacs = awacs.split(' ').next().unwrap();
let str = format!("{} {} {}", awacs, callsign.to_voice(), cmd);
engine.insert(str.to_string(), &str);
}
let results = engine.search(&rawr);
let Some(cmd) = results.first() else {
// no command, casual conversations?
continue;
};
let Ok((_, call)) = parse_call(cmd) else {
// no command, casual conversations?
return;
};
let cmd = call.command;
if config.commands.get(&cmd).is_some() {
let raw = rawr.clone();
let raw = raw.trim().to_string();
command_entered.send(ConsoleCommandEntered {
command_name: cmd.clone(),
raw,
args: vec![],
pilot: ent,
operator: awacs_ent,
});
}
}
}
#[derive(Component)]
pub struct ClientHandler(Receiver<Client>);
#[derive(Debug, Component)]
pub struct Radio {
pub frequency: u64, // the way srs wants it
pub modulation: Modulation,
sguid: String,
voice_sink: Option<Sender<VoiceMessage>>,
message_sink: Option<Sender<MessageRequest>>,
handle: Option<JoinHandle<std::result::Result<(), Box<dyn std::error::Error + Send + Sync>>>>,
}
impl Radio {
pub fn new(frequency: u64, modulation: Modulation) -> Self {
Self {
frequency,
modulation,
sguid: create_sguid(),
voice_sink: None,
message_sink: None,
handle: None,
}
}
}
impl Drop for Radio {
fn drop(&mut self) {
if let Some(handle) = &self.handle {
handle.abort();
}
}
}
async fn whisper(url: &str, data: &[i16]) -> Option<String> {
use byteorder::{LittleEndian, WriteBytesExt};
// convert i16 to u8
let mut result: Vec<u8> = Vec::new();
for &n in data {
let _ = result.write_i16::<LittleEndian>(n);
}
let file_part = reqwest::multipart::Part::bytes(result);
let client = reqwest::ClientBuilder::new()
.timeout(Duration::from_secs(30))
.build()
.unwrap();
let form = reqwest::multipart::Form::new().part("file", file_part);
let res = client
.post(url.to_owned())
.multipart(form)
.header("Content-Type", "multipart/form-data")
.send()
.await;
res.ok()?.text().await.ok()?.trim().to_string().non_empty()
}
trait NonEmpty {
fn non_empty(self) -> Option<String>;
}
impl NonEmpty for String {
fn non_empty(self) -> Option<String> {
if self.is_empty() {
return None;
}
Some(self)
}
}

View File

@@ -0,0 +1,32 @@
// a queue we can push to at will
// we need to take an item and read async,stream?
// pull from the front, push to the back
use std::{collections::VecDeque, sync::Arc};
use tokio::sync::RwLock;
pub struct FrameQueue<T> {
backend: Arc<RwLock<VecDeque<T>>>,
}
impl<T> FrameQueue<T> {
pub fn new() -> Self {
Self {
backend: Arc::new(RwLock::new(VecDeque::new())),
}
}
pub async fn push(&self, item: T) {
self.backend.write().await.push_back(item);
}
pub async fn next(&self) -> Option<T> {
self.backend.write().await.pop_front()
}
}
impl<T> Default for FrameQueue<T> {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,338 @@
use base64::{
alphabet::URL_SAFE,
engine::{general_purpose::NO_PAD, GeneralPurpose},
Engine,
};
use bevy::ecs::component::Component;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::{
collections::HashMap,
error::Error,
fmt::{self, Display},
};
use uuid::Uuid;
pub(crate) const BASE64: GeneralPurpose = base64::engine::GeneralPurpose::new(&URL_SAFE, NO_PAD);
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase", untagged)]
pub enum Message {
Update(UpdateMessage),
Ping(PingMessage),
Sync(SyncMessage),
RadioUpdate(RadioUpdateMessage),
ServerSettings(ServerSettingsMessage),
ClientDisconnect(ClientDisconnectMessage),
VersionMismatch(VersionMismatchMessage),
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase", untagged)]
pub enum MessageRequest {
Update(UpdateMessage),
Sync(SyncMessageRequest),
RadioUpdate(RadioUpdateMessage),
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct UpdateMessage {
pub msg_type: MsgType<0>,
pub client: Client,
pub version: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct PingMessage {
pub msg_type: MsgType<1>,
pub version: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SyncMessage {
pub msg_type: MsgType<2>,
pub clients: Vec<Client>,
pub server_settings: HashMap<String, String>,
pub version: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SyncMessageRequest {
pub msg_type: MsgType<2>,
pub client: Client,
pub version: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct RadioUpdateMessage {
pub msg_type: MsgType<3>,
pub client: Client,
pub version: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ServerSettingsMessage {
pub msg_type: MsgType<4>,
pub server_settings: HashMap<String, String>,
pub version: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ClientDisconnectMessage {
pub msg_type: MsgType<5>,
pub client: Client,
pub version: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct VersionMismatchMessage {
pub msg_type: MsgType<6>,
pub version: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Coalition {
Spectator,
Blue,
Red,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Radio {
pub enc: bool,
pub enc_key: u8,
pub freq: f64,
pub modulation: Modulation,
pub sec_freq: f64,
pub retransmit: bool,
}
impl Default for Radio {
fn default() -> Self {
Radio {
enc: false,
enc_key: 0,
freq: 1.0,
modulation: Modulation::Disabled,
sec_freq: 1.0,
retransmit: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
#[derive(Default)]
pub enum Modulation {
Am = 0,
Fm = 1,
Intercom = 2,
#[default]
Disabled = 3,
HaveQuick = 4,
Satcom = 5,
Mids = 6,
}
#[derive(Component, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RadioInfo {
pub radios: Vec<Radio>,
pub unit: String,
pub unit_id: u32,
pub iff: Transponder,
}
#[derive(Debug, PartialEq, Default, Clone, Copy, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
pub enum RadioSwitchControls {
#[default]
Hotas = 0,
InCockpit = 1,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Client {
pub client_guid: String,
pub name: String,
pub seat: u32,
pub coalition: Coalition,
pub allow_record: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub radio_info: Option<RadioInfo>,
pub lat_lng_position: Position,
}
#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
pub struct Position {
pub lat: f64,
#[serde(rename = "lng")]
pub lon: f64,
pub alt: f64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Transponder {
control: IffControlMode,
mode1: i32,
mode3: i32,
mode4: bool,
mic: i32,
status: IffStatus,
}
#[derive(Debug, Serialize_repr, Deserialize_repr, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum IffControlMode {
Cockpit = 0,
Overlay = 1,
Disabled = 2,
}
#[derive(Debug, Serialize_repr, Deserialize_repr, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum IffStatus {
Off = 0,
Normal = 1,
Ident = 2,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct MsgType<const V: u8>;
#[derive(Debug)]
struct MsgTypeError;
impl Error for MsgTypeError {}
impl Display for MsgTypeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Invalid message type")
}
}
impl<const V: u8> Serialize for MsgType<V> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u8(V)
}
}
impl<'de, const V: u8> Deserialize<'de> for MsgType<V> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = u8::deserialize(deserializer)?;
if value == V {
Ok(MsgType::<V>)
} else {
Err(serde::de::Error::custom(MsgTypeError))
}
}
}
impl Default for Transponder {
fn default() -> Self {
Transponder {
control: IffControlMode::Disabled,
mode1: -1,
mode3: -1,
mode4: false,
mic: -1,
status: IffStatus::Off,
}
}
}
impl ::serde::Serialize for Coalition {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: ::serde::Serializer,
{
// Serialize the enum as a u64.
serializer.serialize_u64(match *self {
Coalition::Spectator => 0,
Coalition::Red => 1,
Coalition::Blue => 2,
})
}
}
impl<'de> ::serde::Deserialize<'de> for Coalition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
struct Visitor;
impl<'de> ::serde::de::Visitor<'de> for Visitor {
type Value = Coalition;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("positive integer or string")
}
fn visit_u64<E>(self, value: u64) -> Result<Coalition, E>
where
E: ::serde::de::Error,
{
// Rust does not come with a simple way of converting a
// number to an enum, so use a big `match`.
match value {
0 => Ok(Coalition::Spectator),
1 => Ok(Coalition::Red),
2 => Ok(Coalition::Blue),
_ => Err(E::custom(format!(
"unknown {} value: {}",
stringify!(Coalition),
value
))),
}
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let value = value.to_lowercase().clone();
let value = value.as_str();
match value {
"spectator" => Ok(Coalition::Spectator),
"red" => Ok(Coalition::Red),
"blue" => Ok(Coalition::Blue),
_ => Err(E::custom(format!(
"unknown {} value: {}",
stringify!(Coalition),
value
))),
}
}
}
// Deserialize the enum from a u64.
deserializer.deserialize_u64(Visitor)
}
}
pub fn create_sguid() -> String {
let sguid = Uuid::new_v4();
// let sguid = base64::encode_config(sguid.as_bytes(), base64::URL_SAFE_NO_PAD);
let sguid = BASE64.encode(sguid.as_bytes());
assert_eq!(sguid.len(), 22);
sguid
}

View File

@@ -0,0 +1,97 @@
use std::{error, fmt, io};
use bytes::BytesMut;
use tokio_util::codec::{Decoder, Encoder, LinesCodec, LinesCodecError};
use super::message::{Message, MessageRequest};
pub struct MessagesCodec {
lines_codec: LinesCodec,
}
impl MessagesCodec {
pub fn new() -> Self {
MessagesCodec {
lines_codec: LinesCodec::new(),
}
}
}
impl Decoder for MessagesCodec {
type Item = Message;
type Error = MessagesCodecError;
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
if let Some(line) = self.lines_codec.decode(buf)? {
match serde_json::from_str(&line) {
Ok(msg) => Ok(Some(msg)),
Err(err) => Err(MessagesCodecError::JsonDecode(err, line)),
}
} else {
Ok(None)
}
}
fn decode_eof(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
if let Some(line) = self.lines_codec.decode_eof(buf)? {
match serde_json::from_str(&line) {
Ok(msg) => Ok(Some(msg)),
Err(err) => Err(MessagesCodecError::JsonDecode(err, line)),
}
} else {
Ok(None)
}
}
}
impl Encoder<MessageRequest> for MessagesCodec {
type Error = MessagesCodecError;
fn encode(&mut self, msg: MessageRequest, buf: &mut BytesMut) -> Result<(), Self::Error> {
let json = serde_json::to_string(&msg).map_err(MessagesCodecError::JsonEncode)?;
self.lines_codec.encode(json, buf)?;
Ok(())
}
}
#[derive(Debug)]
pub enum MessagesCodecError {
JsonDecode(serde_json::Error, String),
JsonEncode(serde_json::Error),
LinesCodec(LinesCodecError),
Io(io::Error),
}
impl fmt::Display for MessagesCodecError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MessagesCodecError::JsonDecode(_, json) => write!(f, "failed to decode JSON: {json}"),
MessagesCodecError::JsonEncode(_) => write!(f, "failed to encode JSON"),
MessagesCodecError::LinesCodec(err) => err.fmt(f),
MessagesCodecError::Io(err) => err.fmt(f),
}
}
}
impl error::Error for MessagesCodecError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
MessagesCodecError::JsonDecode(ref err, _) => Some(err),
MessagesCodecError::JsonEncode(ref err) => Some(err),
MessagesCodecError::LinesCodec(ref err) => Some(err),
MessagesCodecError::Io(ref err) => Some(err),
}
}
}
impl From<io::Error> for MessagesCodecError {
fn from(err: io::Error) -> Self {
MessagesCodecError::Io(err)
}
}
impl From<LinesCodecError> for MessagesCodecError {
fn from(err: LinesCodecError) -> Self {
MessagesCodecError::LinesCodec(err)
}
}

View File

@@ -4,24 +4,11 @@ use std::{
};
use bevy::{
app::Plugin,
ecs::{component::Component, reflect::ReflectComponent},
reflect::{std_traits::ReflectDefault, Reflect},
utils::AHasher,
};
pub struct SrsPlugin;
impl Plugin for SrsPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
todo!()
}
}
fn connect_srs() {
// will emit strings as commands
}
#[derive(Reflect, Component, Clone)]
#[reflect(Component, Default, Debug)]
pub struct VoiceMessage {
@@ -35,6 +22,7 @@ impl Default for VoiceMessage {
}
}
#[allow(unused)]
impl VoiceMessage {
/// Creates a new [`VoiceMessage`] from any string-like type.
///

View File

@@ -0,0 +1,308 @@
use std::{
io::{self, Cursor, Read, Write},
mem::size_of,
};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use bytes::{BufMut, BytesMut};
use tokio_util::codec::{Decoder, Encoder, LengthDelimitedCodec};
pub const GUID_LENGTH: usize = 22;
pub const PACKET_HEADER_LENGTH: usize = size_of::<u16>() // UInt16 Packet Length - 2 bytes
+ size_of::<u16>() // UInt16 AudioPart1 Length - 2 bytes
+ size_of::<u16>(); // UInt16 FrequencyPart Length - 2 bytes
pub const FREQUENCY_SEGMENT_LENGTH: usize = size_of::<f64>() // double Frequency - 8 bytes
+ size_of::<u8>() // byte Modulation - 1 byte
+ size_of::<u8>(); // byte Encryption - 1 byte
pub const FIXED_PACKET_LENGTH: usize = size_of::<u32>() // UInt UnitId - 4 bytes
+ size_of::<u64>() // UInt64 PacketId - 8 bytes
+ size_of::<u8>() // Byte indicating number of hops for this message // default is 0
+ GUID_LENGTH // Bytes / ASCII String Transmission GUID - 22 bytes
+ GUID_LENGTH; // Bytes / ASCII String GUID - 22 bytes
pub const PACKET_NON_AUDIO_DATA_SIZE: usize =
PACKET_HEADER_LENGTH + FIXED_PACKET_LENGTH + FREQUENCY_SEGMENT_LENGTH;
pub struct VoiceCodec {
inner: LengthDelimitedCodec,
is_head: bool,
}
impl VoiceCodec {
pub fn new() -> Self {
VoiceCodec {
inner: LengthDelimitedCodec::builder()
.length_field_offset(0)
.length_field_length(2)
.length_adjustment(-2)
.little_endian()
.new_codec(),
is_head: true,
}
}
}
#[derive(Debug, Clone)]
pub enum Modulation {
Am,
Fm,
Intercom,
Disabled,
}
#[derive(Debug, Clone)]
pub enum Encryption {
None,
JustOverlay,
Full,
CockpitToggleOverlayCode,
}
#[derive(Debug, Clone)]
pub struct Frequency {
pub freq: f64,
pub modulation: Modulation,
pub encryption: Encryption,
}
#[derive(Debug)]
pub enum Packet {
Ping([u8; 22]),
Voice(VoicePacket),
}
#[derive(Debug)]
pub struct VoicePacket {
// TODO: use Bytes instead?
pub audio_part: Vec<u8>,
pub wav_audio_part: Option<Vec<i16>>,
pub frequencies: Vec<Frequency>,
pub unit_id: u32,
pub packet_id: u64,
pub hop_count: u8,
pub transmission_sguid: [u8; 22],
pub client_sguid: [u8; 22],
}
impl Decoder for VoiceCodec {
type Item = VoicePacket;
type Error = io::Error;
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
// discard ping messages
if self.is_head && buf.len() <= 22 {
return Ok(None);
}
if buf.len() < PACKET_NON_AUDIO_DATA_SIZE {
return Ok(None);
}
if let Some(bytes) = self.inner.decode(buf)? {
self.is_head = true;
let len = bytes.len() as u64;
let mut rd = Cursor::new(bytes);
let len_audio_part = rd.read_u16::<LittleEndian>()? as u64;
let len_frequencies = rd.read_u16::<LittleEndian>()? as u64;
assert_eq!(
len,
4 + len_audio_part + len_frequencies + 4 + 8 + 1 + 22 + 22
);
let mut audio_part = vec![0u8; len_audio_part as usize];
rd.read_exact(&mut audio_part)?;
let wav_audio_part = opus_to_wav(&audio_part).unwrap();
let len_before = rd.position();
let mut frequencies = Vec::new();
while rd.position() - len_before < len_frequencies {
let freq = rd.read_f64::<LittleEndian>()?;
let modulation = match rd.read_u8()? {
0 => Modulation::Am,
1 => Modulation::Fm,
2 => Modulation::Intercom,
3 => Modulation::Disabled,
_ => Modulation::Am,
};
let encryption = match rd.read_u8()? {
0 => Encryption::None,
1 => Encryption::JustOverlay,
2 => Encryption::Full,
3 => Encryption::CockpitToggleOverlayCode,
_ => Encryption::None,
};
frequencies.push(Frequency {
freq,
modulation,
encryption,
});
}
let unit_id = rd.read_u32::<LittleEndian>()?;
let packet_id = rd.read_u64::<LittleEndian>()?;
let hop_count = rd.read_u8()?;
let mut transmission_sguid = [0; 22];
rd.read_exact(&mut transmission_sguid)?;
let mut client_sguid = [0; 22];
rd.read_exact(&mut client_sguid)?;
assert_eq!(rd.position(), len);
Ok(Some(VoicePacket {
audio_part,
wav_audio_part: Some(wav_audio_part),
frequencies,
unit_id,
packet_id,
hop_count,
transmission_sguid,
client_sguid,
}))
} else {
self.is_head = false;
Ok(None)
}
}
fn decode_eof(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
self.decode(buf)
}
}
impl Encoder<Packet> for VoiceCodec {
type Error = io::Error;
fn encode(&mut self, packet: Packet, buf: &mut BytesMut) -> Result<(), Self::Error> {
let packet = match packet {
Packet::Ping(sguid) => {
buf.put_slice(&sguid);
return Ok(());
}
Packet::Voice(packet) => packet,
};
// Packet format as specified in
// https://github.com/ciribob/DCS-SimpleRadioStandalone/blob/1.9.3.0/DCS-SR-Common/Network/UDPVoicePacket.cs#L9
/*
* UDP PACKET LAYOUT
*
* - HEADER SEGMENT
* UInt16 Packet Length - 2 bytes
* UInt16 AudioPart1 Length - 2 bytes
* UInt16 FrequencyPart Length - 2 bytes
* - AUDIO SEGMENT
* Bytes AudioPart1 - variable bytes
* - FREQUENCY SEGMENT (one or multiple)
* double Frequency - 8 bytes
* byte Modulation - 1 byte
* byte Encryption - 1 byte
* - FIXED SEGMENT
* UInt UnitId - 4 bytes
* UInt64 PacketId - 8 bytes
* byte Retransmit / node / hop count - 1 byte
* Bytes / ASCII String TRANSMISSION GUID - 22 bytes used for transmission relay
* Bytes / ASCII String CLIENT GUID - 22 bytes
*/
// NOTE: the final packet will start with the total packet length, but this will be added
// by the inner fixed codec
let header_length = 2 + 2;
let frequency_length = 8 + 1 + 1;
let audio_length = packet.audio_part.len();
let fixed_segment_length = 4 + 8 + 1 + 22 + 22;
let capacity = header_length
+ audio_length
+ frequency_length * packet.frequencies.len()
+ fixed_segment_length;
let mut wd = Cursor::new(Vec::with_capacity(capacity));
// header segment will be written at the end
wd.set_position(4);
// - AUDIO SEGMENT
let len_before = wd.position();
wd.write_all(&packet.audio_part)?;
let len_audio_part = wd.position() - len_before;
// - FREQUENCY SEGMENT
let len_before = wd.position();
for f in packet.frequencies {
wd.write_f64::<LittleEndian>(f.freq)?;
wd.write_u8(match f.modulation {
Modulation::Am => 0,
Modulation::Fm => 1,
Modulation::Intercom => 2,
Modulation::Disabled => 3,
})?;
wd.write_u8(match f.encryption {
Encryption::None => 0,
Encryption::JustOverlay => 1,
Encryption::Full => 2,
Encryption::CockpitToggleOverlayCode => 3,
})?;
}
let len_frequency = wd.position() - len_before;
// - FIXED SEGMENT
wd.write_u32::<LittleEndian>(packet.unit_id)?;
wd.write_u64::<LittleEndian>(packet.packet_id)?;
wd.write_u8(packet.hop_count)?; // retransmission hop count
wd.write_all(&packet.transmission_sguid)?; // transmission guid
wd.write_all(&packet.client_sguid)?; // client guid
// - HEADER SEGMENT
wd.set_position(0);
// Packet Length:
// the final packet will start with the total packet length, but this will be added by
// the inner fixed codec
// AudioPart1 Length
wd.write_u16::<LittleEndian>(len_audio_part as u16)?;
// FrequencyPart Length
wd.write_u16::<LittleEndian>(len_frequency as u16)?;
let frame = wd.into_inner();
assert_eq!(frame.len(), capacity);
self.inner.encode(frame.into(), buf)
}
}
impl From<VoicePacket> for Packet {
fn from(p: VoicePacket) -> Self {
Packet::Voice(p)
}
}
const INPUT_SAMPLE_RATE: usize = 16000;
const INPUT_AUDIO_LENGTH: usize = 20; //ms
const FRAME_SIZE: usize = INPUT_SAMPLE_RATE / 1000 * INPUT_AUDIO_LENGTH;
pub(crate) fn opus_to_wav(opus: &[u8]) -> Result<Vec<i16>, Box<dyn std::error::Error>> {
use audiopus::coder::Decoder;
use audiopus::packet::Packet;
use audiopus::{Channels, MutSignals, SampleRate};
let packet: Packet = opus.try_into()?;
let mut pcm_audio_short = vec![0i16; FRAME_SIZE * 2]; //[0i16; FRAME_SIZE];
let signals = MutSignals::try_from(&mut pcm_audio_short)?;
let mut decoder = Decoder::new(SampleRate::Hz16000, Channels::Mono)?;
decoder.decode(Some(packet), signals, false)?; // BadArgument, huh?!
Ok(pcm_audio_short)
}

View File

@@ -0,0 +1,81 @@
use std::{
borrow::Cow,
hash::{Hash, Hasher},
};
use bevy::{
ecs::{component::Component, entity::Entity},
utils::AHasher,
};
#[derive(Component, Clone)]
pub struct VoiceCommand {
hash: u64,
message: Cow<'static, str>,
entity: Entity,
}
#[allow(unused)]
impl VoiceCommand {
/// Creates a new [`VoiceCommand`] from any string-like type.
///
/// The internal hash will be computed immediately.
pub fn new(message: impl Into<Cow<'static, str>>, entity: Entity) -> Self {
let message = message.into();
let mut message = VoiceCommand {
message,
hash: 0,
entity,
};
message.update_hash();
message
}
/// Sets the entity's message.
///
/// The internal hash will be re-computed.
#[inline(always)]
pub fn set(&mut self, message: impl Into<Cow<'static, str>>) {
*self = VoiceCommand::new(message, self.entity);
}
pub fn get_entity(&self) -> &Entity {
&self.entity
}
/// Updates the message of the entity in place.
///
/// This will allocate a new string if the message was previously
/// created from a borrow.
#[inline(always)]
pub fn mutate<F: FnOnce(&mut String)>(&mut self, f: F) {
f(self.message.to_mut());
self.update_hash();
}
/// Gets the message of the entity as a `&str`.
#[inline(always)]
pub fn as_str(&self) -> &str {
&self.message
}
fn update_hash(&mut self) {
let mut hasher = AHasher::default();
self.message.hash(&mut hasher);
self.hash = hasher.finish();
}
}
impl std::fmt::Display for VoiceCommand {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(&self.message, f)
}
}
impl std::fmt::Debug for VoiceCommand {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.message, f)
}
}

View File

@@ -0,0 +1,160 @@
// TODO: Look into SpeakNG to avoid the windows dependency https://crates.io/crates/espeakng
use std::borrow::Cow;
use tokio::sync::Mutex;
use windows::core::HSTRING;
use windows::Media::SpeechSynthesis::SpeechSynthesizer;
use windows::Storage::Streams::DataReader;
#[derive(Debug)]
pub struct WinConfig {
pub voice: Option<String>,
}
impl WinConfig {
pub fn new() -> Self {
let voice = "David"; // for now
Self {
voice: Some(voice.to_string()),
}
}
}
static MUTEX: Mutex<()> = Mutex::const_new(());
pub async fn synthesize(text: &str) -> Result<Vec<Vec<u8>>, WinError> {
let config = WinConfig::new();
// Note, there does not seem to be a way to explicitly set 16000kHz, 16 audio bits per
// sample and mono channel.
// Prevent concurrent Windows TTS synthesis, as this might cause a crash.
let lock = MUTEX.lock().await;
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();
println!("Using WIN voice: {}", name);
voice_info = Some(v);
break;
}
}
if voice_info.is_none() {
println!("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;
println!(
"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();
println!("- {} ({})", 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")
};
// 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 rd = DataReader::CreateDataReader(&stream.GetInputStreamAt(0)?)?;
rd.LoadAsync(size as u32)?.await?;
let mut wav = vec![0u8; size as usize];
rd.ReadBytes(wav.as_mut_slice())?;
drop(lock);
Ok(wav_to_opus(wav.into()).await?)
}
#[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),
}
impl From<windows::core::Error> for WinError {
fn from(err: windows::core::Error) -> Self {
WinError::Win(err.code().0, err.message().to_string())
}
}
async fn wav_to_opus(wav: bytes::Bytes) -> Result<Vec<Vec<u8>>, audiopus::Error> {
use audiopus::coder::Encoder;
use audiopus::{Application, Channels, SampleRate};
tokio::task::spawn_blocking(move || {
let audio_stream = wav
.chunks(2)
.map(|bytes| i16::from_le_bytes(bytes.try_into().unwrap()))
.collect::<Vec<_>>();
const MONO_20MS: usize = 16000 /* 1 channel */ * 20 / 1000;
let enc = Encoder::new(SampleRate::Hz16000, Channels::Mono, Application::Voip)?;
let mut pos = 0;
let mut output = [0; 256];
let mut frames = Vec::new();
while pos + MONO_20MS < audio_stream.len() {
let len = enc.encode(&audio_stream[pos..(pos + MONO_20MS)], &mut output)?;
frames.push(output[..len].to_vec());
pos += MONO_20MS;
}
Ok::<_, audiopus::Error>(frames)
})
.await
.unwrap()
}