zello_client/
utilities.rs1use std::collections::VecDeque;
7use std::sync::Arc;
8
9use crate::{
10 CPAL_BUFFER_SIZE, CPAL_CHANNELS, CPAL_SAMPLE_RATE, CPAL_VECTOR_QUEUE_CAPACITY, OPUS_CHANNELS,
11 OPUS_SAMPLE_RATE, PCM_I16_TO_F32,
12};
13use crate::{Credentials, ZelloClient, ZelloConfig};
14use anyhow::{Result, anyhow};
15use audiopus::coder::Decoder;
16use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
17use cpal::{Device, Stream, StreamConfig};
18use crossbeam_channel::Receiver;
19use dotenvy::{dotenv, from_path};
20use tokio::sync::Mutex;
21use tracing::{debug, error, info};
22use tracing_subscriber::EnvFilter;
23
24pub fn initialize_logging() -> Result<()> {
26 tracing_subscriber::fmt()
27 .with_env_filter(
28 EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
29 )
30 .init();
31 Ok(())
32}
33
34pub fn load_dotenv() -> Result<()> {
40 dotenv().map_err(|e| anyhow!("Warning: Could not load .env file {e}"))?;
41 Ok(())
42}
43
44pub fn load_dotenv_from_file(path: &str) -> Result<()> {
50 from_path(path).map_err(|e| anyhow!("Warning: Could not load '{path}' file {e}"))?;
51 Ok(())
52}
53
54pub fn load_credentials() -> Result<Credentials> {
60 let username = std::env::var("ZELLO_USERNAME")
61 .map_err(|_| anyhow!("Please set ZELLO_USERNAME environment variable"))?;
62
63 let password = std::env::var("ZELLO_PASSWORD")
64 .map_err(|_| anyhow!("Please set ZELLO_PASSWORD environment variable"))?;
65
66 let token = std::env::var("ZELLO_TOKEN")
67 .map_err(|_| anyhow!("Please set ZELLO_TOKEN environment variable"))?;
68
69 let channel = std::env::var("ZELLO_CHANNEL")
70 .map_err(|_| anyhow!("Please set ZELLO_CHANNEL environment variable"))?;
71
72 Ok(Credentials {
73 username,
74 password,
75 token,
76 channel,
77 })
78}
79
80pub fn create_decoder() -> Result<Arc<Mutex<Decoder>>> {
86 let decoder = Decoder::new(OPUS_SAMPLE_RATE, OPUS_CHANNELS)?;
87 Ok(Arc::new(Mutex::new(decoder)))
88}
89
90pub fn get_audio_device() -> Result<Device> {
96 let host = cpal::default_host();
97 host.default_output_device()
98 .ok_or_else(|| anyhow!("No output device found"))
99}
100
101#[must_use]
103pub fn create_stream_config() -> StreamConfig {
104 StreamConfig {
105 channels: CPAL_CHANNELS,
106 sample_rate: CPAL_SAMPLE_RATE,
107 buffer_size: CPAL_BUFFER_SIZE,
108 }
109}
110
111pub fn setup_audio_output(pcm_rx: Arc<Mutex<Receiver<Vec<i16>>>>) -> Result<Stream> {
117 let device = get_audio_device()?;
118 let stream_config = create_stream_config();
119
120 let mut buffer = VecDeque::<f32>::with_capacity(CPAL_VECTOR_QUEUE_CAPACITY);
121 let err_fn = |err| error!("Stream error: {err:?}");
122
123 let stream = device.build_output_stream(
124 &stream_config,
125 move |output: &mut [f32], _: &cpal::OutputCallbackInfo| {
126 process_audio_output(output, &pcm_rx, &mut buffer);
127 },
128 err_fn,
129 None,
130 )?;
131
132 stream.play()?;
133 Ok(stream)
134}
135
136pub fn process_audio_output(
138 output: &mut [f32],
139 pcm_rx: &Arc<Mutex<Receiver<Vec<i16>>>>,
140 buffer: &mut VecDeque<f32>,
141) {
142 if let Ok(rx) = pcm_rx.try_lock() {
144 while let Ok(pcm) = rx.try_recv() {
145 debug!("🎤 Received PCM chunk: {} samples", pcm.len());
146 for &sample in &pcm {
147 buffer.push_back(f32::from(sample) * PCM_I16_TO_F32);
148 }
149 }
150 }
151
152 debug!(
153 "🎤 Output buffer size: {}, internal buffer: {}",
154 output.len(),
155 buffer.len()
156 );
157
158 for out in output.iter_mut() {
160 *out = buffer.pop_front().unwrap_or(0.0);
161 }
162}
163
164pub async fn connect_to_zello(credentials: &Credentials) -> Result<ZelloClient> {
170 info!("Connecting to Zello...");
171 info!("Username: {}", credentials.username);
172 info!("Channel: {}", credentials.channel);
173
174 let config = ZelloConfig::new(
175 credentials.username.clone(),
176 credentials.password.clone(),
177 credentials.token.clone(),
178 credentials.channel.clone(),
179 );
180
181 match ZelloClient::new(config).await {
182 Ok(client) => {
183 info!("✓ Connected and authenticated successfully!");
184 Ok(client)
185 }
186 Err(e) => Err(anyhow!("✗ Failed to connect or authenticate: {e}")),
187 }
188}