Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ jobs:
if: hashFiles('Cargo.lock') == ''
run: cargo generate-lockfile
- name: cargo llvm-cov
run: cargo llvm-cov --workspace --doctests --locked --lcov --output-path lcov.info
run: cargo llvm-cov --workspace --doctests --all-features --locked --lcov --output-path lcov.info
- name: Upload to codecov.io
uses: codecov/codecov-action@v4
with:
Expand Down
18 changes: 16 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,9 @@ serde = ["serde/derive", "bytes?/serde", "enumflags2?/serde"]

[dependencies]
bytes = { version = "1.9.0", default-features = false, optional = true }
nom = { version = "8.0.0", default-features = false }
zbus = { version = "5.0", default-features = false, optional = true, features = ["async-io"] }
serde = { version = "1.0", default-features = false, optional = true }
enumflags2 = { version = "0.7.11", default-features = false, optional = true }
static_assertions = "1.1.0"
serde_repr = { version = "0.1.20", optional = true }

[dev-dependencies]
Expand All @@ -35,6 +33,7 @@ assert_matches = { version = "1.5.0", default-features = false }
spiel = { path = ".", default-features = false }
hound = "3.5.1"
itertools = "0.14.0"
proptest = { version = "1.6.0", default-features = false, features = ["std", "attr-macro"] }

[[example]]
name = "filter_audio_data"
Expand All @@ -46,6 +45,11 @@ name = "list_voices"
path = "./examples/list_voices.rs"
required-features = ["client"]

[[example]]
name = "list_voices_with_client"
path = "./examples/list_voices_client.rs"
required-features = ["client"]

[[example]]
name = "write_to_file"
path = "./examples/write_something_to_files.rs"
Expand All @@ -55,3 +59,13 @@ required-features = ["client"]
name = "write_to_pipe"
path = "./examples/write_something_to_pipe.rs"
required-features = ["client"]

[[example]]
name = "make_provider"
path = "./examples/provider.rs"
required-features = ["client"]

[[example]]
name = "test_provider"
path = "./examples/test_provider.rs"
required-features = ["client"]
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ Read and write audio, as well as mixed audio/events streams using the Spiel spee

Note that features with an unmarked checkbox are not yet implemented.

- [X] `default`: none. This includes all basic protocol functionality, both from bytes and into bytes: `no_std` and `no_alloc`. It depends only on the `nom` and `memchr` crates.
- [X] `default`: none. This includes all basic protocol functionality, both from bytes and into bytes: `no_std` and `no_alloc`. This feature set requires only `core`.
- [X] `client`: `std`, and pulls in the [`zbus`](https://crates.io/crates/zbus) crate. This provides a `Client` proxy type that ask for the speech provider to synthesize some speech, as well as query which voices and options are available.
- [X] `reader`: `alloc`. This gives you a sans-io `Reader` type where you can [`Reader::push`] bytes into the buffer, and then [`Reader::try_read`] to the conversion into a [`Message`].
- This is _almost_ zero-copy. But currently requires a clone of the string if an event sent from the synthesizer has a name.
- [X] `alloc`: pulls in the [`bytes`](https://crates.io/crates/bytes), if `serde` is enabled. It exposes new types like [`crate::Message`] and [`crate::Event`], which are owned versions of [`crate::MessageBorrow`] and [`crate::EventBorrow`].
- [X] `alloc`: pulls in the [`bytes`](https://crates.io/crates/bytes), if `serde` is enabled. It exposes new types like [`crate::MessageOwned`] and [`crate::EventOwned`], which are owned versions of [`crate::Message`] and [`crate::Event`].
- [X] `poll`: add wrapper functions that return `Poll::Pending` when there is not enough data in the buffer. This is not for general use, but rather only if you are creating an async integration.
- [X] `serde`: activate [`serde::Serialize`] and [`serde::Deserialize`] on all types.
- [ ] `provider`: activates [`std`] and pulls in the [`zbus`](https://crates.io/crates/zbus) crate. This will provide the `SpeechProvider` struct, which can be used to provide speech over the Spiel protocol via `DBus`.
Expand Down
12 changes: 5 additions & 7 deletions examples/filter_audio_data.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use hound::{SampleFormat, WavSpec, WavWriter};
use itertools::Itertools;
use spiel::{read_message_type, MessageType};
use spiel::{read_message, Message};

fn main() {
let mut data: &[u8] = include_bytes!("../test.wav");
Expand All @@ -13,16 +13,14 @@ fn main() {
};
let mut writer = WavWriter::create("out.wav", spec).expect("Can make wave writer!");
for _ in 0..55 {
let (data_next, msg) =
read_message_type(data, header).expect("to be able to read data");
let (offset, msg) = read_message(data, header).expect("to be able to read data");
header = true;
if let MessageType::Audio { samples_offset, samples_len } = msg {
let ch = &data[samples_offset..samples_len];
for (l, h) in ch.iter().tuples() {
if let Message::Audio(samples) = msg {
for (l, h) in samples.iter().tuples() {
let sample = i16::from_le_bytes([*l, *h]);
writer.write_sample(sample).expect("Can write to file");
}
}
data = data_next;
data = &data[offset..];
}
}
20 changes: 20 additions & 0 deletions examples/list_voices_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use std::error::Error;

use spiel::client::Client;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let client = Client::new().await?;
let providers = client.list_providers().await?;
for provider in providers {
let pname = provider.name().await?;
println!("Provider: {pname}");
for voice in provider.voices().await? {
println!("\t{}", voice.name);
for lang in voice.languages {
println!("\t\t{lang}");
}
}
}
Ok(())
}
92 changes: 92 additions & 0 deletions examples/provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use std::{
io::{PipeWriter, Write},
os::fd::OwnedFd,
time::Duration,
};

use spiel::{write_message, Event, EventType, Message, Voice, VoiceFeatureSet};
use tokio::time::sleep;
use zbus::{connection::Builder, fdo::Error, interface, zvariant::Fd};

struct MySpeechProvider {
voices: Vec<Voice>,
}

#[interface(name = "org.freedesktop.Speech.Provider")]
impl MySpeechProvider {
#[zbus(property)]
async fn voices(&self) -> Vec<Voice> {
self.voices.clone()
}
#[zbus(property)]
async fn name(&self) -> String {
"Silly Provider!".to_string()
}
#[allow(clippy::too_many_arguments)]
async fn synthesize(
&self,
pipe_fd: Fd<'_>,
_text: &str,
_voice_id: &str,
_pitch: f64,
_rate: f64,
_is_ssml: bool,
_language: &str,
) {
println!("Received a syntheiszer event!");
// find a voice that matches the language &str
// actually synthesize text,
// etc.
//
// We are just gonna write a simple `Message::Event`
let header = Message::Version("0.01");
let msg = Message::Event(Event {
typ: EventType::Word,
start: 69,
end: 420,
name: Some("Hello :)"),
});
// buffer has fixed size in this case
let mut buffer: [u8; 1024] = [0; 1024];
let writer: OwnedFd = pipe_fd
.try_into()
.map_err(|_| Error::IOError("Cannot open file descriptor".to_string()))
.expect("Unable to open file descriptor!");
let mut file = PipeWriter::from(writer);
// TODO: implement a more convenient way to not have to store a buffer, etc.
let offset =
write_message(&header, &mut buffer).expect("Unable to write to buffer!");
let bytes_written_buf = write_message(&msg, &mut buffer[offset..])
.expect("Unable to write to buffer!");
let bytes_written_fd = file
.write(&buffer[..bytes_written_buf + offset])
.map_err(|_| Error::IOError("Cannot write to file descriptor".to_string()))
.expect("Unable to write to file descriptor!");
println!("Wrote {bytes_written_fd} bytes to Fd");
assert_eq!(bytes_written_buf + offset, bytes_written_fd);
}
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let voice = Voice {
name: "My Voice".to_string(),
id: "my-voice".to_string(),
mime_format: "audio/x-spiel,format=S32LE,channels=1,rate=22050".to_string(),
features: VoiceFeatureSet::empty(),
// English, New Zealand
languages: vec!["en-NZ".to_string()],
};
let voices = Vec::from([voice]);
let provider = MySpeechProvider { voices };
let _connection = Builder::session()?
.name("org.domain.Speech.Provider")?
.serve_at("/org/domain/Speech/Provider", provider)?
.build()
.await?;

// wait forever for 60 seconds to test another program can receive the event!
println!("Started provider! Go check if it works using the `test-provider` example!");
sleep(Duration::from_secs(60)).await;
Ok(())
}
53 changes: 53 additions & 0 deletions examples/test_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//! This tool can be run with the `examples/provider.rs` binary to check the the service shows up
//! on DBus.
//! And that methods can be sent and dealt with appropriately.

use std::{error::Error, io, io::Read, os::fd::OwnedFd};

use spiel::{read_message, Client, Event, EventType, Message};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let client = Client::new().await?;
let (mut reader, writer_pipe) = io::pipe()?;
let writer = OwnedFd::from(writer_pipe);
let providers = client.list_providers().await?;
let mut found = false;
for provider in providers {
if provider.inner().destination() != "org.domain.Speech.Provider" {
continue;
}
found = true;
print!("TRY SEND...");
provider.synthesize(
writer.into(), // pipe writer
"my-voice", // voice ID
"Hello!", // text to synthesize
0.5, // pitch
0.5, // rate
false, // SSML on
"en-NZ", // English, New Zealand
)
.await?;
println!("SENT!");
let mut buf = Vec::new();
let bytes_read = reader.read_to_end(&mut buf)?;
println!("BYTES READ: {bytes_read}");
let (bytes_read2, header) = read_message(&buf[..], false)?;
let (bytes_read3, msg) = read_message(&buf[bytes_read2..], true)?;
assert_eq!(bytes_read, bytes_read2 + bytes_read3);
assert_eq!(header, Message::Version("0.01"));
assert_eq!(
msg,
Message::Event(Event {
typ: EventType::Word,
start: 69,
end: 420,
name: Some("Hello :)"),
})
);
break;
}
assert!(found, "Could not find org.domain.Speech.Provider!");
Ok(())
}
8 changes: 8 additions & 0 deletions proptest-regressions/protocol.txt

Large diffs are not rendered by default.

Loading