commit 8ab8cdedbd1b2919978bdf0afcc9eb8efd1cb145 Author: Tony Klink Date: Tue Feb 20 18:08:40 2024 -0600 Initial commit diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c782e06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +*.svg +*.data +*.old +/bin \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ae67ef8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "live-link-face" +version = "0.1.0" +dependencies = [ + "byteorder", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..fdfb10a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "live-link-face" +version = "0.1.0" +authors = ["Klink"] +description = "Live Link Face packets encoder/decoder" +repository = "https://git.zhitno.st/klink/live-link-face" +keywords = ["unreal", "livelink", "face-tracking"] +edition = "2021" + +[dependencies] +byteorder = "1.5.0" + +[profile.release] +opt-level = 'z' # Optimize for size +lto = true # Enable link-time optimization +codegen-units = 1 # Reduce number of codegen units to increase optimizations +panic = 'abort' # Abort on panic +strip = true # Strip symbols from binary* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6345d61 --- /dev/null +++ b/LICENSE @@ -0,0 +1,8 @@ +Copyright (C) 2024 Klink +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3. + +The above copyright notice, this permission notice and the word "NIGGER" shall be included in all copies or substantial portions of the Software. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with this program. If not, see \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5b19bc --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Live Link Face Rust + +Live Link Face Rust is a library for decoding/encoding UDP dadagrams sent by Live Link Face App. +You get `LiveLinkFace` struct from which you can get any blend shape: +``` +let face_data = LiveLinkFace::decode(&BUF); // UDP datagram; decode returns tuple of (bool, LiveLinkFace) +assert!(face_data.0); // 'true' means that there is face data. false - returns empty LiveLinkFace object +let eye_blink_left = face_data.1.get_blendshape(FaceBlendShape::EyeBlinkLeft); +``` + +This library is inspired by https://github.com/JimWest/PyLiveLinkFace diff --git a/examples/listener.rs b/examples/listener.rs new file mode 100644 index 0000000..c3782d6 --- /dev/null +++ b/examples/listener.rs @@ -0,0 +1,270 @@ +use live_link_face::{LiveLinkFace, FaceBlendShape}; +use std::io::{prelude::*, stdout}; +use std::net::UdpSocket; + +fn main() -> std::io::Result<()> { + let socket = UdpSocket::bind("[::]:3000")?; // for UDP4/6 + let mut buf = [0; 1024]; + + loop { + // Receives a single datagram message on the socket. + // If `buf` is too small to hold + // the message, it will be cut off. + let (amt, _src) = socket.recv_from(&mut buf)?; + + // Redeclare `buf` as slice of the received data + // and send data back to origin. + let buf = &mut buf[..amt]; + let face = LiveLinkFace::decode(buf); + shitty_print(face.1); + } +} + +fn shitty_print(face_data: LiveLinkFace) { + print!("{}[2J", 27 as char); + stdout().flush().unwrap(); + println!( + "EyeBlinkLeft: {}", + face_data.get_blendshape(FaceBlendShape::EyeBlinkLeft) + ); + println!( + "EyeLookDownLeft: {}", + face_data.get_blendshape(FaceBlendShape::EyeLookDownLeft) + ); + println!( + "EyeLookInLeft: {}", + face_data.get_blendshape(FaceBlendShape::EyeLookInLeft) + ); + println!( + "EyeLookOutLeft: {}", + face_data.get_blendshape(FaceBlendShape::EyeLookOutLeft) + ); + println!( + "EyeLookUpLeft: {}", + face_data.get_blendshape(FaceBlendShape::EyeLookUpLeft) + ); + println!( + "EyeSquintLeft: {}", + face_data.get_blendshape(FaceBlendShape::EyeSquintLeft) + ); + println!( + "EyeWideLeft: {}", + face_data.get_blendshape(FaceBlendShape::EyeWideLeft) + ); + println!( + "EyeBlinkRight: {}", + face_data.get_blendshape(FaceBlendShape::EyeBlinkRight) + ); + println!( + "EyeLookDownRight: {}", + face_data.get_blendshape(FaceBlendShape::EyeLookDownRight) + ); + println!( + "EyeLookInRight: {}", + face_data.get_blendshape(FaceBlendShape::EyeLookInRight) + ); + println!( + "EyeLookOutRight: {}", + face_data.get_blendshape(FaceBlendShape::EyeLookOutRight) + ); + println!( + "EyeLookUpRight: {}", + face_data.get_blendshape(FaceBlendShape::EyeLookUpRight) + ); + println!( + "EyeSquintRight: {}", + face_data.get_blendshape(FaceBlendShape::EyeSquintRight) + ); + println!( + "EyeWideRight: {}", + face_data.get_blendshape(FaceBlendShape::EyeWideRight) + ); + println!( + "JawForward: {}", + face_data.get_blendshape(FaceBlendShape::JawForward) + ); + println!( + "JawLeft: {}", + face_data.get_blendshape(FaceBlendShape::JawLeft) + ); + println!( + "JawRight: {}", + face_data.get_blendshape(FaceBlendShape::JawRight) + ); + println!( + "JawOpen: {}", + face_data.get_blendshape(FaceBlendShape::JawOpen) + ); + println!( + "MouthClose: {}", + face_data.get_blendshape(FaceBlendShape::MouthClose) + ); + println!( + "MouthFunnel: {}", + face_data.get_blendshape(FaceBlendShape::MouthFunnel) + ); + println!( + "MouthPucker: {}", + face_data.get_blendshape(FaceBlendShape::MouthPucker) + ); + println!( + "MouthLeft: {}", + face_data.get_blendshape(FaceBlendShape::MouthLeft) + ); + println!( + "MouthRight: {}", + face_data.get_blendshape(FaceBlendShape::MouthRight) + ); + println!( + "MouthSmileLeft: {}", + face_data.get_blendshape(FaceBlendShape::MouthSmileLeft) + ); + println!( + "MouthSmileRight: {}", + face_data.get_blendshape(FaceBlendShape::MouthSmileRight) + ); + println!( + "MouthFrownLeft: {}", + face_data.get_blendshape(FaceBlendShape::MouthFrownLeft) + ); + println!( + "MouthFrownRight: {}", + face_data.get_blendshape(FaceBlendShape::MouthFrownRight) + ); + println!( + "MouthDimpleLeft: {}", + face_data.get_blendshape(FaceBlendShape::MouthDimpleLeft) + ); + println!( + "MouthDimpleRight: {}", + face_data.get_blendshape(FaceBlendShape::MouthDimpleRight) + ); + println!( + "MouthStretchLeft: {}", + face_data.get_blendshape(FaceBlendShape::MouthStretchLeft) + ); + println!( + "MouthStretchRight: {}", + face_data.get_blendshape(FaceBlendShape::MouthStretchRight) + ); + println!( + "MouthRollLower: {}", + face_data.get_blendshape(FaceBlendShape::MouthRollLower) + ); + println!( + "MouthRollUpper: {}", + face_data.get_blendshape(FaceBlendShape::MouthRollUpper) + ); + println!( + "MouthShrugLower: {}", + face_data.get_blendshape(FaceBlendShape::MouthShrugLower) + ); + println!( + "MouthShrugUpper: {}", + face_data.get_blendshape(FaceBlendShape::MouthShrugUpper) + ); + println!( + "MouthPressLeft: {}", + face_data.get_blendshape(FaceBlendShape::MouthPressLeft) + ); + println!( + "MouthPressRight: {}", + face_data.get_blendshape(FaceBlendShape::MouthPressRight) + ); + println!( + "MouthLowerDownLeft: {}", + face_data.get_blendshape(FaceBlendShape::MouthLowerDownLeft) + ); + println!( + "MouthLowerDownRight: {}", + face_data.get_blendshape(FaceBlendShape::MouthLowerDownRight) + ); + println!( + "MouthUpperUpLeft: {}", + face_data.get_blendshape(FaceBlendShape::MouthUpperUpLeft) + ); + println!( + "MouthUpperUpRight: {}", + face_data.get_blendshape(FaceBlendShape::MouthUpperUpRight) + ); + println!( + "BrowDownLeft: {}", + face_data.get_blendshape(FaceBlendShape::BrowDownLeft) + ); + println!( + "BrowDownRight: {}", + face_data.get_blendshape(FaceBlendShape::BrowDownRight) + ); + println!( + "BrowInnerUp: {}", + face_data.get_blendshape(FaceBlendShape::BrowInnerUp) + ); + println!( + "BrowOuterUpLeft: {}", + face_data.get_blendshape(FaceBlendShape::BrowOuterUpLeft) + ); + println!( + "BrowOuterUpRight: {}", + face_data.get_blendshape(FaceBlendShape::BrowOuterUpRight) + ); + println!( + "CheekPuff: {}", + face_data.get_blendshape(FaceBlendShape::CheekPuff) + ); + println!( + "CheekSquintLeft: {}", + face_data.get_blendshape(FaceBlendShape::CheekSquintLeft) + ); + println!( + "CheekSquintRight: {}", + face_data.get_blendshape(FaceBlendShape::CheekSquintRight) + ); + println!( + "NoseSneerLeft: {}", + face_data.get_blendshape(FaceBlendShape::NoseSneerLeft) + ); + println!( + "NoseSneerRight: {}", + face_data.get_blendshape(FaceBlendShape::NoseSneerRight) + ); + println!( + "TongueOut: {}", + face_data.get_blendshape(FaceBlendShape::TongueOut) + ); + println!( + "HeadYaw: {}", + face_data.get_blendshape(FaceBlendShape::HeadYaw) + ); + println!( + "HeadPitch: {}", + face_data.get_blendshape(FaceBlendShape::HeadPitch) + ); + println!( + "HeadRoll: {}", + face_data.get_blendshape(FaceBlendShape::HeadRoll) + ); + println!( + "LeftEyeYaw: {}", + face_data.get_blendshape(FaceBlendShape::LeftEyeYaw) + ); + println!( + "LeftEyePitch: {}", + face_data.get_blendshape(FaceBlendShape::LeftEyePitch) + ); + println!( + "LeftEyeRoll: {}", + face_data.get_blendshape(FaceBlendShape::LeftEyeRoll) + ); + println!( + "RightEyeYaw: {}", + face_data.get_blendshape(FaceBlendShape::RightEyeYaw) + ); + println!( + "RightEyePitch: {}", + face_data.get_blendshape(FaceBlendShape::RightEyePitch) + ); + println!( + "RightEyeRoll: {}", + face_data.get_blendshape(FaceBlendShape::RightEyeRoll) + ); +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..564b1d3 --- /dev/null +++ b/flake.lock @@ -0,0 +1,93 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "naersk": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1698420672, + "narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=", + "owner": "nix-community", + "repo": "naersk", + "rev": "aeb58d5e8faead8980a807c840232697982d47b9", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "naersk", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1708296515, + "narHash": "sha256-FyF489fYNAUy7b6dkYV6rGPyzp+4tThhr80KNAaF/yY=", + "path": "/nix/store/2p9qcybzmx1rfayjw866rz8xljbw45g9-source", + "rev": "b98a4e1746acceb92c509bc496ef3d0e5ad8d4aa", + "type": "path" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1708407374, + "narHash": "sha256-EECzarm+uqnNDCwaGg/ppXCO11qibZ1iigORShkkDf0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f33dd27a47ebdf11dc8a5eb05e7c8fbdaf89e73f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "naersk": "naersk", + "nixpkgs": "nixpkgs_2" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..bd04450 --- /dev/null +++ b/flake.nix @@ -0,0 +1,42 @@ +{ + inputs = { + flake-utils.url = "github:numtide/flake-utils"; + naersk.url = "github:nix-community/naersk"; + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + }; + + outputs = { self, flake-utils, naersk, nixpkgs }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = (import nixpkgs) { inherit system; }; + + naersk' = pkgs.callPackage naersk { }; + + in rec { + # For `nix build` & `nix run`: + packages.default = naersk'.buildPackage { + src = ./.; + nativeBuildInputs = with pkgs; [ pkg-config openssl ]; + GIT_HASH = "000000000000000000000000000000"; + }; + + # For `nix develop`: + devShells.default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + rustc + cargo + cargo-watch + clippy + rustfmt + rust-analyzer + pkg-config + + openssl + ]; + env = { + RUST_BACKTRACE = 1; + RUST_LOG = "debug"; + }; + }; + }); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d03333c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,240 @@ +use byteorder::{BigEndian, ByteOrder, LittleEndian}; +use std::collections::VecDeque; +use std::time::{SystemTime, UNIX_EPOCH}; + +const FILTER_SIZE: usize = 5; + +#[allow(dead_code)] +#[derive(Debug, PartialEq)] +pub enum FaceBlendShape { + EyeBlinkLeft = 0, + EyeLookDownLeft = 1, + EyeLookInLeft = 2, + EyeLookOutLeft = 3, + EyeLookUpLeft = 4, + EyeSquintLeft = 5, + EyeWideLeft = 6, + EyeBlinkRight = 7, + EyeLookDownRight = 8, + EyeLookInRight = 9, + EyeLookOutRight = 10, + EyeLookUpRight = 11, + EyeSquintRight = 12, + EyeWideRight = 13, + JawForward = 14, + JawLeft = 15, + JawRight = 16, + JawOpen = 17, + MouthClose = 18, + MouthFunnel = 19, + MouthPucker = 20, + MouthLeft = 21, + MouthRight = 22, + MouthSmileLeft = 23, + MouthSmileRight = 24, + MouthFrownLeft = 25, + MouthFrownRight = 26, + MouthDimpleLeft = 27, + MouthDimpleRight = 28, + MouthStretchLeft = 29, + MouthStretchRight = 30, + MouthRollLower = 31, + MouthRollUpper = 32, + MouthShrugLower = 33, + MouthShrugUpper = 34, + MouthPressLeft = 35, + MouthPressRight = 36, + MouthLowerDownLeft = 37, + MouthLowerDownRight = 38, + MouthUpperUpLeft = 39, + MouthUpperUpRight = 40, + BrowDownLeft = 41, + BrowDownRight = 42, + BrowInnerUp = 43, + BrowOuterUpLeft = 44, + BrowOuterUpRight = 45, + CheekPuff = 46, + CheekSquintLeft = 47, + CheekSquintRight = 48, + NoseSneerLeft = 49, + NoseSneerRight = 50, + TongueOut = 51, + HeadYaw = 52, + HeadPitch = 53, + HeadRoll = 54, + LeftEyeYaw = 55, + LeftEyePitch = 56, + LeftEyeRoll = 57, + RightEyeYaw = 58, + RightEyePitch = 59, + RightEyeRoll = 60, +} + +#[derive(Debug)] +pub struct LiveLinkFace { + pub uuid: String, + pub name: String, + pub fps: i32, + version: u32, + frames: u32, + sub_frame: i32, + denominator: i32, + blend_shapes: Vec, + old_blend_shapes: Vec>, +} + +impl LiveLinkFace { + pub fn new(name: String, uuid: String, fps: i32) -> Self { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards"); + let frames = now.as_secs() as u32; + let sub_frame = 1056060032; // I don't know why + let denominator = fps / 60; // 1 most of the time + + let mut old_blend_shapes: Vec> = Vec::with_capacity(61); + for _ in 0..61 { + old_blend_shapes.push(VecDeque::with_capacity(FILTER_SIZE)); + } + + LiveLinkFace { + uuid, + name, + fps, + version: 6, + frames, + sub_frame, + denominator, + blend_shapes: vec![0.0; 61], + old_blend_shapes, + } + } + + pub fn encode(&self) -> Vec { + let mut encoded: Vec = Vec::new(); + encoded.extend(&(self.version).to_le_bytes()); + encoded.extend(self.uuid.as_bytes()); + encoded.extend(&(self.name.len() as i32).to_be_bytes()); + encoded.extend(self.name.as_bytes()); + + // Prepare frame data + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards"); + let frame_time = now.as_secs() as u32; + let frame_rate = (self.fps, self.denominator); + let blend_shapes_data = self.blend_shapes.to_vec(); + + encoded.extend(&(frame_time).to_be_bytes()); + encoded.extend(&(self.sub_frame).to_be_bytes()); + encoded.extend(&(frame_rate).0.to_be_bytes()); + encoded.extend(&(frame_rate).1.to_be_bytes()); + encoded.extend(&(blend_shapes_data.len() as i32).to_be_bytes()); + for f in &blend_shapes_data { + encoded.extend(&f.to_be_bytes()); + } + + encoded + } + + pub fn get_blendshape(&self, index: FaceBlendShape) -> f32 { + self.blend_shapes[index as usize] + } + + pub fn set_blendshape(&mut self, index: FaceBlendShape, value: f32, no_filter: bool) { + let idx = index as usize; + if no_filter { + self.blend_shapes[idx] = value; + } else { + self.old_blend_shapes[idx].push_back(value); + let filtered_value: f32 = self.old_blend_shapes[idx].iter().sum::() + / self.old_blend_shapes[idx].len() as f32; + self.blend_shapes[idx] = filtered_value; + } + } + + pub fn decode(data: &[u8]) -> (bool, LiveLinkFace) { + let version = LittleEndian::read_i16(&data[0..4]); + let uuid = String::from_utf8_lossy(&data[4..41]).to_string(); + let name_length = BigEndian::read_i32(&data[41..45]) as usize; + let name_end_pos = 45 + name_length; + let name = String::from_utf8_lossy(&data[45..name_end_pos]).to_string(); + + if data.len() > name_end_pos + 16 { + let frame_number = BigEndian::read_i32(&data[name_end_pos..name_end_pos + 4]) as usize; + let sub_frame = BigEndian::read_i32(&data[name_end_pos + 4..name_end_pos + 8]); + let fps = BigEndian::read_i32(&data[name_end_pos + 8..name_end_pos + 12]); + let denominator = BigEndian::read_i32(&data[name_end_pos + 12..name_end_pos + 16]); + let (int_bytes, _rest) = data[70..74].split_at(std::mem::size_of::()); + let data_length = i8::from_be_bytes(int_bytes.try_into().unwrap()); + + if data_length != 61 { + println!( + "Blendshape data is malformed. got: {}; requred: 61", + data_length + ); + } + + let mut blend_shapes: [f32; 61] = [0.0; 61]; + for i in 0..61 { + blend_shapes[i] = BigEndian::read_f32( + &data[name_end_pos + 17 + i * 4..name_end_pos + 17 + (i + 1) * 4], + ); + } + + // println!("vata: {:?}", vata); + let mut live_link_face = LiveLinkFace::new(name, uuid, fps); + live_link_face.version = version as u32; + live_link_face.frames = frame_number as u32; + live_link_face.sub_frame = sub_frame; + live_link_face.denominator = denominator; + live_link_face.blend_shapes = blend_shapes.to_vec(); + + return (true, live_link_face); + } + + ( + false, + LiveLinkFace::new("Default".to_string(), "Default".to_string(), 60), + ) + } +} + +#[cfg(test)] +mod tests { + use crate::{LiveLinkFace, FaceBlendShape}; + + const BUF: [u8;315] = [ + 0x06, 0x00, 0x00, 0x00, 0x24, 0x30, 0x30, 0x46, 0x44, 0x45, 0x31, 0x43, 0x33, 0x2D, + 0x46, 0x41, 0x38, 0x35, 0x2D, 0x34, 0x30, 0x36, 0x37, 0x2D, 0x39, 0x37, 0x44, 0x38, + 0x2D, 0x31, 0x31, 0x33, 0x37, 0x43, 0x41, 0x31, 0x38, 0x33, 0x30, 0x35, 0x45, 0x00, + 0x00, 0x00, 0x09, 0x46, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x63, 0x6B, 0x00, 0x00, + 0xFD, 0x2F, 0x3E, 0x02, 0xF6, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, + 0x3D, 0x39, 0xC0, 0x37, 0x00, 0x3D, 0x95, 0x3A, 0xAE, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x39, 0xC0, 0x95, 0xF3, 0x3D, 0x95, 0x2D, 0x4E, 0x3D, 0x02, 0xA5, 0x4E, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x39, 0x7B, 0x26, 0xB0, 0x3B, 0x0D, 0x5D, 0x4F, 0x00, 0x00, 0x00, 0x00, 0x3B, + 0x1D, 0xB8, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3B, 0x3B, 0xE3, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3A, 0xFD, 0xE5, + 0x7A, 0x3B, 0x09, 0x19, 0x78, 0x3B, 0x64, 0x88, 0xAE, 0x3B, 0x2E, 0x7D, 0xE2, 0x00, + 0x00, 0x00, 0x00, 0x3A, 0xD8, 0xD2, 0xAF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBD, + 0xE8, 0xCC, 0x08, 0xBE, 0x39, 0x6A, 0x4C, 0x3C, 0x36, 0xE9, 0x45, 0xBC, 0x9D, 0xC5, + 0x24, 0x3D, 0x2A, 0x20, 0x39, 0xBA, 0xE9, 0xF6, 0xC7, 0xBC, 0x9B, 0x6C, 0xD2, 0x3D, + 0x2A, 0x1E, 0x4B, 0x3A, 0xA1, 0xB0, 0x42, + ]; + #[test] + fn decode_buf() { + let face_data = LiveLinkFace::decode(&BUF); + + assert!(face_data.0); + assert_eq!(face_data.1.get_blendshape(FaceBlendShape::EyeBlinkLeft), 0.00036662072); + } +} +