From cde73248ec3c61b04a6f016fcb85f50e175c044e Mon Sep 17 00:00:00 2001 From: Winter Hille Date: Fri, 29 Aug 2025 18:04:07 -0700 Subject: [PATCH] initial --- .gitignore | 1 + Cargo.lock | 407 +++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 23 +++ LICENSE | 18 +++ README.md | 0 flake.lock | 66 ++++++++ flake.nix | 72 +++++++++ src/lib.rs | 375 +++++++++++++++++++++++++++++++++++++++++++++ sys/Cargo.toml | 18 +++ sys/build.rs | 27 ++++ sys/src/lib.rs | 4 + 11 files changed, 1011 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 src/lib.rs create mode 100644 sys/Cargo.toml create mode 100644 sys/build.rs create mode 100644 sys/src/lib.rs 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"));