zello_client/
utilities.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// SPDX-FileCopyrightText: 2024 John C. Murray
3
4//! Utility functions for Zello client operations
5
6use 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
24/// Initialize logging with environment filter
25pub 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
34/// Load environment variables from .env file
35///
36/// # Errors
37///
38/// Returns an error if the required .env file is not found or cannot be loaded
39pub fn load_dotenv() -> Result<()> {
40    dotenv().map_err(|e| anyhow!("Warning: Could not load .env file {e}"))?;
41    Ok(())
42}
43
44/// Load environment variables from a specific file
45///
46/// # Errors
47///
48/// Returns an error if the required file is not found or cannot be loaded
49pub 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
54/// Load Zello credentials from environment variables
55///
56/// # Errors
57///
58/// Returns an error if required environment variables are not set
59pub 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
80/// Create an Opus audio decoder
81///
82/// # Errors
83///
84/// Returns an error if decoder creation fails
85pub 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
90/// Get the default audio output device
91///
92/// # Errors
93///
94/// Returns an error if no output device is found
95pub 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/// Create audio stream configuration
102#[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
111/// Setup audio output stream
112///
113/// # Errors
114///
115/// Returns an error if stream creation or playback fails
116pub 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
136/// Process audio output by filling the output buffer
137pub fn process_audio_output(
138    output: &mut [f32],
139    pcm_rx: &Arc<Mutex<Receiver<Vec<i16>>>>,
140    buffer: &mut VecDeque<f32>,
141) {
142    // Try to refill buffer from channel
143    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    // Fill output from buffer
159    for out in output.iter_mut() {
160        *out = buffer.pop_front().unwrap_or(0.0);
161    }
162}
163
164/// Connect to Zello and authenticate
165///
166/// # Errors
167///
168/// Returns an error if connection or authentication fails
169pub 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}