Initial commit

This commit is contained in:
Tony Klink 2024-02-20 18:08:40 -06:00
commit 8ab8cdedbd
Signed by: klink
GPG key ID: 85175567C4D19231
10 changed files with 704 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
/target
*.svg
*.data
*.old
/bin

16
Cargo.lock generated Normal file
View file

@ -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",
]

18
Cargo.toml Normal file
View file

@ -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*

8
LICENSE Normal file
View file

@ -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 <https://www.gnu.org/licenses/>

11
README.md Normal file
View file

@ -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

270
examples/listener.rs Normal file
View file

@ -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)
);
}

93
flake.lock Normal file
View file

@ -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
}

42
flake.nix Normal file
View file

@ -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";
};
};
});
}

240
src/lib.rs Normal file
View file

@ -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<f32>,
old_blend_shapes: Vec<VecDeque<f32>>,
}
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<VecDeque<f32>> = 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<u8> {
let mut encoded: Vec<u8> = 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::<f32>()
/ 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::<i8>());
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);
}
}