use super::formats::AudioCompressionFormat;
use super::types::{AudioClip, AudioClipMeta, StreamingInfo};
use crate::error::{BinaryError, Result};
use crate::object::UnityObject;
use crate::unity_version::UnityVersion;
use unity_asset_core::UnityValue;
pub struct AudioClipConverter {
version: UnityVersion,
}
impl AudioClipConverter {
pub fn new(version: UnityVersion) -> Self {
Self { version }
}
pub fn from_unity_object(&self, obj: &UnityObject) -> Result<AudioClip> {
if let Ok(clip) = self.try_parse_typetree(obj) {
return Ok(clip);
}
self.parse_binary_data(obj.raw_data())
}
fn try_parse_typetree(&self, obj: &UnityObject) -> Result<AudioClip> {
fn as_i32(v: &UnityValue) -> Option<i32> {
v.as_i64().and_then(|n| i32::try_from(n).ok())
}
fn as_u64(v: &UnityValue) -> Option<u64> {
v.as_i64().and_then(|n| u64::try_from(n).ok())
}
fn as_u32(v: &UnityValue) -> Option<u32> {
v.as_i64().and_then(|n| u32::try_from(n).ok())
}
fn as_f32(v: &UnityValue) -> Option<f32> {
v.as_f64().map(|n| n as f32)
}
let props = obj.class.properties();
let name = props
.get("m_Name")
.and_then(|v| v.as_str())
.ok_or_else(|| BinaryError::invalid_data("AudioClip missing m_Name"))?
.to_string();
let channels = props.get("m_Channels").and_then(as_i32).unwrap_or(2);
let frequency = props.get("m_Frequency").and_then(as_i32).unwrap_or(44100);
let bits_per_sample = props.get("m_BitsPerSample").and_then(as_i32).unwrap_or(16);
let length = props.get("m_Length").and_then(as_f32).unwrap_or(0.0);
let load_type = props.get("m_LoadType").and_then(as_i32).unwrap_or(0);
let is_tracker_format = props
.get("m_IsTrackerFormat")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let subsound_index = props.get("m_SubsoundIndex").and_then(as_i32).unwrap_or(0);
let preload_audio_data = props
.get("m_PreloadAudioData")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let load_in_background = props
.get("m_LoadInBackground")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let legacy_3d = props
.get("m_Legacy3D")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let compression_format_val = props
.get("m_CompressionFormat")
.and_then(as_i32)
.unwrap_or(0);
let compression_format = AudioCompressionFormat::from(compression_format_val);
let mut clip = AudioClip {
name,
meta: AudioClipMeta::Modern {
load_type,
channels,
frequency,
bits_per_sample,
length,
is_tracker_format,
subsound_index,
preload_audio_data,
load_in_background,
legacy_3d,
compression_format,
},
ambisonic: props.get("m_Ambisonic").and_then(|v| v.as_bool()),
..Default::default()
};
if let Some(v) = props.get("m_AudioData") {
match v {
UnityValue::Bytes(b) => clip.data = b.clone(),
UnityValue::Array(items) => {
let mut bytes = Vec::with_capacity(items.len());
for item in items {
if let Some(n) = item.as_i64() {
bytes.push(n.clamp(0, 255) as u8);
}
}
clip.data = bytes;
}
_ => {}
}
}
if let Some(UnityValue::Object(res)) = props.get("m_Resource") {
let source = res
.get("m_Source")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let offset = res.get("m_Offset").and_then(as_u64).unwrap_or(0);
let size = res.get("m_Size").and_then(as_u32).unwrap_or(0);
if !source.is_empty() && size > 0 {
clip.stream_info = StreamingInfo {
offset,
size,
path: source.clone(),
};
clip.source = Some(source);
clip.offset = offset;
clip.size = size as u64;
}
}
if clip.data.is_empty() && !clip.is_streamed() {
return Err(BinaryError::invalid_data(
"AudioClip typetree did not contain audio bytes or stream resource info",
));
}
Ok(clip)
}
#[allow(clippy::field_reassign_with_default)]
fn parse_binary_data(&self, data: &[u8]) -> Result<AudioClip> {
if data.is_empty() {
return Err(BinaryError::invalid_data("Empty audio data"));
}
let mut reader = crate::reader::BinaryReader::new(data, crate::reader::ByteOrder::Little);
let mut clip = AudioClip::default();
clip.name = reader
.read_aligned_string()
.unwrap_or_else(|_| "UnknownAudio".to_string());
if self.version.major < 5 {
let format = reader.read_i32().unwrap_or(0);
let type_ = reader.read_i32().unwrap_or(0);
let is_3d = reader.read_bool().unwrap_or(false);
let use_hardware = reader.read_bool().unwrap_or(false);
clip.meta = AudioClipMeta::Legacy {
format,
type_,
is_3d,
use_hardware,
};
} else {
let load_type = reader.read_i32().unwrap_or(0);
let channels = reader.read_i32().unwrap_or(2);
let frequency = reader.read_i32().unwrap_or(44100);
let bits_per_sample = reader.read_i32().unwrap_or(16);
let length = reader.read_f32().unwrap_or(0.0);
let is_tracker_format = reader.read_bool().unwrap_or(false);
let _ = reader.align();
let subsound_index = reader.read_i32().unwrap_or(0);
let preload_audio_data = reader.read_bool().unwrap_or(true);
let load_in_background = reader.read_bool().unwrap_or(false);
let legacy_3d = reader.read_bool().unwrap_or(false);
let _ = reader.align();
let mut compression_format = AudioCompressionFormat::Unknown;
let mut compression_format_read = false;
let mut resource_source = String::new();
let mut resource_offset = 0u64;
let mut resource_size = 0u32;
let resource_pos = reader.position();
let mut parsed_resource = false;
if let Ok(source) = reader.read_aligned_string() {
let looks_like_source = source.is_empty()
|| source.contains("archive:/")
|| source.contains('/')
|| source.contains('\\')
|| source.ends_with(".resS")
|| source.ends_with(".resource");
if looks_like_source {
resource_source = source;
resource_offset = reader.read_u64().unwrap_or(0);
resource_size = reader.read_u32().unwrap_or(0);
let _ = reader.align();
parsed_resource = true;
}
}
if !parsed_resource {
let _ = reader.set_position(resource_pos);
let compression_format_val = reader.read_i32().unwrap_or(-1);
compression_format = AudioCompressionFormat::from(compression_format_val);
compression_format_read = true;
if self.version.major >= 2017 {
clip.ambisonic = reader.read_bool().ok();
let _ = reader.align();
}
resource_source = reader.read_aligned_string().unwrap_or_default();
resource_offset = reader.read_u64().unwrap_or(0);
resource_size = reader.read_u32().unwrap_or(0);
let _ = reader.align();
}
if !resource_source.is_empty() && resource_size > 0 {
clip.stream_info = StreamingInfo {
offset: resource_offset,
size: resource_size,
path: resource_source.clone(),
};
clip.source = Some(resource_source);
clip.offset = resource_offset;
clip.size = resource_size as u64;
}
let data_size = reader.read_u32().unwrap_or(0);
if data_size > 0 && reader.remaining() >= data_size as usize {
clip.data = reader.read_bytes(data_size as usize).unwrap_or_default();
} else if !clip.is_streamed() && reader.remaining() > 0 {
let remaining_data = reader.read_remaining();
clip.data = remaining_data.to_vec();
}
if !compression_format_read
&& reader.remaining() >= 4
&& let Ok(val) = reader.read_i32()
&& (-1..=25).contains(&val)
{
compression_format = AudioCompressionFormat::from(val);
}
clip.meta = AudioClipMeta::Modern {
load_type,
channels,
frequency,
bits_per_sample,
length,
is_tracker_format,
subsound_index,
preload_audio_data,
load_in_background,
legacy_3d,
compression_format,
};
}
clip.size = if clip.is_streamed() {
clip.stream_info.size as u64
} else {
clip.data.len() as u64
};
Ok(clip)
}
pub fn supported_formats(&self) -> Vec<AudioCompressionFormat> {
let mut formats = vec![
AudioCompressionFormat::PCM,
AudioCompressionFormat::Vorbis,
AudioCompressionFormat::ADPCM,
];
if self.version.major >= 4 {
formats.push(AudioCompressionFormat::MP3);
}
if self.version.major >= 5 {
formats.push(AudioCompressionFormat::AAC);
}
formats
}
pub fn can_process(&self, format: AudioCompressionFormat) -> bool {
self.supported_formats().contains(&format)
}
pub fn load_streaming_data(&self, clip: &AudioClip) -> Result<Vec<u8>> {
if clip.stream_info.path.is_empty() {
return Err(BinaryError::invalid_data("No streaming path specified"));
}
use std::fs;
use std::path::Path;
let stream_path = Path::new(&clip.stream_info.path);
let possible_paths = [
stream_path.to_path_buf(),
Path::new("StreamingAssets").join(stream_path),
Path::new("..").join(stream_path),
];
for path in &possible_paths {
if path.exists() {
match fs::File::open(path) {
Ok(mut file) => {
use std::io::{Read, Seek, SeekFrom};
if file.seek(SeekFrom::Start(clip.stream_info.offset)).is_err() {
continue; }
let mut buffer = vec![0u8; clip.stream_info.size as usize];
match file.read_exact(&mut buffer) {
Ok(_) => return Ok(buffer),
Err(_) => continue, }
}
Err(_) => continue, }
}
}
Err(BinaryError::generic(format!(
"Could not load streaming data from: {}",
clip.stream_info.path
)))
}
pub fn get_audio_data(&self, clip: &AudioClip) -> Result<Vec<u8>> {
if !clip.data.is_empty() {
Ok(clip.data.clone())
} else if clip.is_streamed() {
self.load_streaming_data(clip)
} else {
Err(BinaryError::invalid_data("No audio data available"))
}
}
}
pub type AudioClipProcessor = AudioClipConverter;