commit cde73248ec3c61b04a6f016fcb85f50e175c044e Author: Winter Hille Date: Fri Aug 29 18:04:07 2025 -0700 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5d50d0b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,407 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + +[[package]] +name = "ggwave" +version = "0.1.0" +dependencies = [ + "bitflags", + "ggwave-sys", + "libc", +] + +[[package]] +name = "ggwave-sys" +version = "0.0.1" +dependencies = [ + "bindgen", + "libc", + "shell-words", + "tempfile", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "wasi" +version = "0.14.3+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1ff3a98 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[workspace] +resolver = "2" +members = ["sys"] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "MIT" + +[package] +name = "ggwave" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +bitflags = "2.9" +ggwave-sys.path = "sys" +libc = "0.2" + +[features] +default = [] +less_protocls = [] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d817195 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..0b40ebc --- /dev/null +++ b/flake.lock @@ -0,0 +1,66 @@ +{ + "nodes": { + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1755585599, + "narHash": "sha256-tl/0cnsqB/Yt7DbaGMel2RLa7QG5elA8lkaOXli6VdY=", + "owner": "nix-community", + "repo": "fenix", + "rev": "6ed03ef4c8ec36d193c18e06b9ecddde78fb7e42", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1756386758, + "narHash": "sha256-1wxxznpW2CKvI9VdniaUnTT2Os6rdRJcRUf65ZK9OtE=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "dfb2f12e899db4876308eba6d93455ab7da304cd", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "fenix": "fenix", + "nixpkgs": "nixpkgs" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1755504847, + "narHash": "sha256-VX0B9hwhJypCGqncVVLC+SmeMVd/GAYbJZ0MiiUn2Pk=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "a905e3b21b144d77e1b304e49f3264f6f8d4db75", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..487aeb1 --- /dev/null +++ b/flake.nix @@ -0,0 +1,72 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + fenix = { + url = "github:nix-community/fenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + fenix, + }: + let + pkgs = nixpkgs.legacyPackages.x86_64-linux // { + ggwave = pkgs.stdenv.mkDerivation rec { + pname = "ggwave"; + version = "0.4.1"; + + src = pkgs.fetchFromGitHub { + owner = "ggerganov"; + repo = "ggwave"; + rev = "${pname}-v${version}"; + hash = "sha256-tymG9JC+Anb3D26EwMF8laz8xvGnrEA+tqMmGBAnY7o="; + fetchSubmodules = true; + }; + + nativeBuildInputs = [ pkgs.cmake ]; + + cmakeFlags = [ + "-DGGWAVE_BUILD_EXAMPLES=OFF" + "-DCMAKE_BUILD_TYPE=Release" + ]; + + doCheck = true; + + installPhase = '' + # for some reason nix won't copy the include even though cmake tells it to + mkdir -p $out/include + cp -r $src/include/* $out/include + cmake --install . --prefix=$out + ''; + }; + }; + + rustPkgs = fenix.packages.x86_64-linux.default; + in + with pkgs; + { + devShells.x86_64-linux.default = mkShell { + packages = [ + pkg-config + cmake + clang + pkgs.ggwave + ] + ++ (with rustPkgs; [ + cargo + clippy + rustc + rustfmt + ]); + + GGWAVE_PATH = "${pkgs.ggwave}"; + LD_LIBRARY_PATH = "${pkgs.ggwave}/lib"; + LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + }; + }; +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..1b6ebb6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,375 @@ +use std::{ffi::CString, path::Path}; + +use bitflags::bitflags; +pub use ggwave_sys as ffi; + +type Str = &'static str; + +#[derive(Debug)] +pub enum Error { + InitError, + EncodeError(Str), + DecodeError(Str), +} + +impl std::error::Error for Error {} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InitError => write!(f, "Failed to initalize GGWave"), + Error::EncodeError(message) => write!(f, "Encode error: {}", message), + Error::DecodeError(message) => write!(f, "Decode error: {}", message), + } + } +} + +pub type Result = std::result::Result; + +#[repr(C)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum SampleFormat { + Undefined, + U8, + I8, + U16, + I16, + F32, +} + +impl From for SampleFormat { + fn from(value: u32) -> Self { + unsafe { std::mem::transmute(value) } + } +} + +impl From for u32 { + fn from(value: SampleFormat) -> Self { + unsafe { std::mem::transmute(value) } + } +} + +#[cfg(feature = "less_protocls")] +#[repr(C)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum ProtocolId { + DtNormal, + DtFast, + DtFastest, + MtNormal, + MtFast, + MtFastest, +} + +#[cfg(not(feature = "less_protocls"))] +#[repr(C)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum ProtocolId { + AudibleNormal, + AudibleFast, + AudibleFastest, + UltrasoundNormal, + UltrasoundFast, + UltrasoundFastest, + DtNormal, + DtFast, + DtFastest, + MtNormal, + MtFast, + MtFastest, +} + +impl Default for ProtocolId { + #[cfg(feature = "less_protocls")] + fn default() -> Self { + ProtocolId::DtFast + } + + #[cfg(not(feature = "less_protocls"))] + fn default() -> Self { + ProtocolId::AudibleFast + } +} + +impl From for ProtocolId { + fn from(value: u32) -> Self { + unsafe { std::mem::transmute(value) } + } +} + +impl From for u32 { + fn from(value: ProtocolId) -> Self { + unsafe { std::mem::transmute(value) } + } +} + +bitflags! { + #[derive(Debug, Copy, Clone, Eq, PartialEq)] + pub struct OperatingMode: u32 { + const RX = ffi::GGWAVE_OPERATING_MODE_RX; + const TX = ffi::GGWAVE_OPERATING_MODE_TX; + const RX_AND_TX = ffi::GGWAVE_OPERATING_MODE_RX_AND_TX; + const TX_ONLY_TONES = ffi::GGWAVE_OPERATING_MODE_TX_ONLY_TONES; + const USE_DSS = ffi::GGWAVE_OPERATING_MODE_USE_DSS; + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Parameters(ffi::ggwave_Parameters); + +impl Parameters { + pub fn payload_length(&self) -> i32 { + self.0.payloadLength + } + + pub fn set_payload_length(&mut self, length: i32) { + self.0.payloadLength = length; + } + + pub fn sample_rate_in(&self) -> f32 { + self.0.sampleRateInp + } + + pub fn set_sample_rate_in(&mut self, rate: f32) { + self.0.sampleRateInp = rate; + } + + pub fn sample_rate_out(&self) -> f32 { + self.0.sampleRateOut + } + + pub fn set_sample_rate_out(&mut self, rate: f32) { + self.0.sampleRateOut = rate; + } + + pub fn sample_rate(&self) -> f32 { + self.0.sampleRate + } + + pub fn set_sample_rate(&mut self, rate: f32) { + self.0.sampleRate = rate; + } + + pub fn samples_per_frame(&self) -> i32 { + self.0.samplesPerFrame + } + + pub fn set_samples_per_frame(&mut self, samples: i32) { + self.0.samplesPerFrame = samples + } + + pub fn sound_marker_threshold(&self) -> f32 { + self.0.soundMarkerThreshold + } + + pub fn set_sound_marker_threshold(&mut self, threshold: f32) { + self.0.soundMarkerThreshold = threshold; + } + + pub fn sample_format_in(&self) -> SampleFormat { + SampleFormat::from(self.0.sampleFormatInp) + } + + pub fn set_sample_format_in(&mut self, format: SampleFormat) { + self.0.sampleFormatInp = format as u32; + } + + pub fn sample_format_out(&self) -> SampleFormat { + SampleFormat::from(self.0.sampleFormatOut) + } + + pub fn set_sample_format_out(&mut self, format: SampleFormat) { + self.0.sampleFormatOut = format as u32; + } + + pub fn operating_mode(&self) -> OperatingMode { + OperatingMode::from_bits(self.0.operatingMode as u32).unwrap() + } + + pub fn set_operating_mode(&mut self, operating_mode: OperatingMode) { + self.0.operatingMode = operating_mode.bits() as i32; + } +} + +impl Default for Parameters { + fn default() -> Self { + unsafe { Parameters(ffi::ggwave_getDefaultParameters()) } + } +} + +pub fn set_log_file(file: Option<&Path>) { + unsafe { + let file_ptr = if let Some(file) = file { + let file = CString::new(file.to_str().unwrap()).unwrap(); + libc::fopen(file.as_ptr(), c"w".as_ptr()) + } else { + std::ptr::null_mut() + }; + + ffi::GGWave_setLogFile(file_ptr as *mut _); + } +} + +#[derive(Debug)] +pub struct GGWave(ffi::GGWave); + +impl GGWave { + pub fn new(parameters: Parameters) -> Self { + unsafe { GGWave(ffi::GGWave::new(¶meters.0 as *const _)) } + } + + pub fn builder() -> GGWaveBuilder { + GGWaveBuilder::new() + } + + pub fn encode(&mut self, payload: &[u8], protocol_id: ProtocolId) -> Result> { + unsafe { + if !ffi::GGWave_init1( + &mut self.0, + payload.len() as i32, + payload.as_ptr() as *const _, + protocol_id as u32, + 100, + ) { + return Err(Error::InitError); + } + + let mut waveform = Vec::new(); + waveform.resize(ffi::GGWave_encodeSize_bytes(&self.0) as usize, 0); + + let n_bytes = ffi::GGWave_encode(&mut self.0); + if n_bytes == 0 { + return Err(Error::EncodeError("Failed to encode")); + } + + std::ptr::copy_nonoverlapping( + ffi::GGWave_txWaveform(&self.0), + waveform.as_mut_ptr() as *mut _, + n_bytes as usize, + ); + + Ok(waveform) + } + } + + pub fn decode(&mut self, waveform: &[u8]) -> Result> { + unsafe { + if !ffi::GGWave_decode( + &mut self.0, + waveform.as_ptr() as *const _, + waveform.len() as u32, + ) { + return Err(Error::DecodeError("Failed to decode")); + } + + let mut payload = ffi::ggvector::default(); + if ffi::GGWave_rxTakeData(&mut self.0, &mut payload) <= 0 { + return Err(Error::DecodeError("Failed to decode")); + } + + Ok(std::slice::from_raw_parts(payload.m_data, payload.m_size as usize).to_vec()) + } + } +} + +impl Default for GGWave { + fn default() -> Self { + GGWave::new(Parameters::default()) + } +} + +unsafe impl Send for GGWave {} +unsafe impl Sync for GGWave {} + +pub struct GGWaveBuilder { + params: Parameters, +} + +impl GGWaveBuilder { + pub fn new() -> Self { + GGWaveBuilder { + params: Parameters::default(), + } + } + + pub fn payload_length(mut self, length: i32) -> Self { + self.params.set_payload_length(length); + self + } + + pub fn sample_rate_in(mut self, rate: f32) -> Self { + self.params.set_sample_rate_in(rate); + self + } + + pub fn sample_rate_out(mut self, rate: f32) -> Self { + self.params.set_sample_rate_out(rate); + self + } + + pub fn sample_rate(mut self, rate: f32) -> Self { + self.params.set_sample_rate(rate); + self + } + + pub fn samples_per_frame(mut self, samples: i32) -> Self { + self.params.set_samples_per_frame(samples); + self + } + + pub fn sound_marker_threshold(mut self, threshold: f32) -> Self { + self.params.set_sound_marker_threshold(threshold); + self + } + + pub fn sample_format_in(mut self, format: SampleFormat) -> Self { + self.params.set_sample_format_in(format); + self + } + + pub fn sample_format_out(mut self, format: SampleFormat) -> Self { + self.params.set_sample_format_out(format); + self + } + + pub fn operating_mode(mut self, mode: OperatingMode) -> Self { + self.params.set_operating_mode(mode); + self + } + + pub fn build(self) -> GGWave { + GGWave::new(self.params) + } +} + +#[cfg(test)] +mod tests { + use crate as ggwave; + use ggwave::GGWave; + + #[test] + fn test_encode() { + ggwave::set_log_file(None); + let mut ggwave = GGWave::default(); + + let waveform = ggwave + .encode(b"test", ggwave::ProtocolId::default()) + .expect("Failed to encode"); + + assert!(!waveform.is_empty()); + } + + #[test] + fn test_decode() { + ggwave::set_log_file(None); + let mut ggwave = GGWave::default(); + + let waveform = ggwave + .encode(b"test", ggwave::ProtocolId::default()) + .expect("Failed to encode"); + + let payload = ggwave.decode(&waveform).expect("Failed to decode"); + + assert_eq!(b"test", payload.as_slice()); + } +} diff --git a/sys/Cargo.toml b/sys/Cargo.toml new file mode 100644 index 0000000..2ee6b1d --- /dev/null +++ b/sys/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ggwave-sys" +version = "0.0.1" +edition.workspace = true +build = "build.rs" + +[dependencies] +libc = "0.2" + +[build-dependencies] +bindgen = "0.72" + +[dev-dependencies] +shell-words = "1.0.0" +tempfile = "3" + +[features] +default = [] diff --git a/sys/build.rs b/sys/build.rs new file mode 100644 index 0000000..ffbb5ff --- /dev/null +++ b/sys/build.rs @@ -0,0 +1,27 @@ +use std::{env, path::Path}; + +fn main() { + let out_dir = env::var("OUT_DIR").unwrap(); + let out_dir = Path::new(&out_dir); + + // TODO: add better error handling for when GGWAVE_PATH is not set + let ggwave_path = env::var("GGWAVE_PATH").unwrap(); + let ggwave_path = Path::new(&ggwave_path); + + let ggwave_lib_dir = ggwave_path.join("lib"); + let ggwave_include_dir = ggwave_path.join("include"); + + println!("cargo:rustc-link-search=native={ggwave_lib_dir:?}"); + println!("cargo:rustc-link-lib=ggwave"); + + let bindings = bindgen::Builder::default() + .header(ggwave_include_dir.join("ggwave/ggwave.h").to_string_lossy()) + .clang_args(["-x", "c++"]) + .derive_debug(true) + .derive_default(true) + .generate() + .unwrap(); + + let bindings_path = out_dir.join("bindings.rs"); + bindings.write_to_file(&bindings_path).unwrap(); +} diff --git a/sys/src/lib.rs b/sys/src/lib.rs new file mode 100644 index 0000000..8ecdd38 --- /dev/null +++ b/sys/src/lib.rs @@ -0,0 +1,4 @@ +#![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] +#![allow(clippy::missing_safety_doc)] + +include!(concat!(env!("OUT_DIR"), "/bindings.rs"));