omg so much has changed idk

This commit is contained in:
AviiNL
2024-01-18 20:50:27 +01:00
parent f0f0d0c550
commit b0efc215ba
20 changed files with 544 additions and 1104 deletions

1026
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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"

View File

@@ -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;

View File

@@ -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) => {

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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>();
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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())
}

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "stable"

View File

@@ -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
)
}

View File

@@ -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));

View File

@@ -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));

View File

@@ -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));

View File

@@ -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));

View File

@@ -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"),
},
);

View File

@@ -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![],
// });
// }
// }

View File

@@ -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));