Skip to content

Commit 7a772ad

Browse files
committed
feat(AudioClipConverter - fmod): move fmod libs into a separate project
1 parent a52f70d commit 7a772ad

File tree

24 files changed

+21
-214
lines changed

24 files changed

+21
-214
lines changed
Lines changed: 14 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,14 @@
11
from __future__ import annotations
22

3-
import ctypes
4-
import os
5-
import platform
6-
from threading import Lock
73
from typing import TYPE_CHECKING, Dict
84

9-
from UnityPy.streams import EndianBinaryWriter
5+
import fmod_toolkit
106

117
from ..helpers.ResourceReader import get_resource_data
128

13-
try:
14-
import numpy as np
15-
except ImportError:
16-
np = None
17-
import struct
18-
199
if TYPE_CHECKING:
2010
from ..classes import AudioClip
2111

22-
# pyfmodex loads the dll/so/dylib on import
23-
# so we have to adjust the environment vars
24-
# before importing it
25-
# This is done in import_pyfmodex()
26-
# which will replace the global pyfmodex var
27-
pyfmodex = None
28-
29-
30-
def get_fmod_path(
31-
system: str, # "Windows", "Linux", "Darwin"
32-
arch: str, # "x64", "x86", "arm", "arm64"
33-
) -> str:
34-
if system == "Darwin":
35-
# universal dylib
36-
return "lib/FMOD/Darwin/libfmod.dylib"
37-
if system == "Windows":
38-
return f"lib/FMOD/Windows/{arch}/fmod.dll"
39-
if system == "Linux":
40-
if arch == "x64":
41-
arch = "x86_64"
42-
return f"lib/FMOD/Linux/{arch}/libfmod.so"
43-
44-
raise NotImplementedError(f"Unsupported system: {system}")
45-
46-
47-
def import_pyfmodex():
48-
global pyfmodex
49-
if pyfmodex is not None:
50-
return
51-
52-
# determine system - Windows, Darwin, Linux, Android
53-
system = platform.system()
54-
arch = platform.architecture()[0]
55-
machine = platform.machine()
56-
57-
if "arm" in machine:
58-
arch = "arm"
59-
elif "aarch64" in machine:
60-
if system == "Linux":
61-
arch = "arm64"
62-
else:
63-
arch = "arm"
64-
elif arch == "32bit":
65-
arch = "x86"
66-
elif arch == "64bit":
67-
arch = "x64"
68-
69-
fmod_rel_path = get_fmod_path(system, arch)
70-
fmod_path = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), fmod_rel_path)
71-
os.environ["PYFMODEX_DLL_PATH"] = fmod_path
72-
73-
# build path and load library
74-
# prepare the environment for pyfmodex
75-
if system != "Windows":
76-
# hotfix ctypes for pyfmodex for non windows systems
77-
ctypes.windll = None
78-
79-
import pyfmodex
80-
8112

8213
def extract_audioclip_samples(audio: AudioClip, convert_pcm_float: bool = True) -> Dict[str, bytes]:
8314
"""extracts all the samples from an AudioClip
@@ -86,16 +17,20 @@ def extract_audioclip_samples(audio: AudioClip, convert_pcm_float: bool = True)
8617
:return: {filename : sample(bytes)}
8718
:rtype: dict
8819
"""
20+
audio_data: bytes
8921
if audio.m_AudioData:
90-
audio_data = audio.m_AudioData
91-
else:
22+
audio_data = bytes(audio.m_AudioData)
23+
elif audio.m_Resource:
24+
assert audio.object_reader is not None, "AudioClip uses an external resource but object_reader is not set"
9225
resource = audio.m_Resource
9326
audio_data = get_resource_data(
9427
resource.m_Source,
9528
audio.object_reader.assets_file,
9629
resource.m_Offset,
9730
resource.m_Size,
9831
)
32+
else:
33+
raise ValueError("AudioClip with neither m_AudioData nor m_Resource")
9934

10035
magic = memoryview(audio_data)[:8]
10136
if magic[:4] == b"OggS":
@@ -104,127 +39,11 @@ def extract_audioclip_samples(audio: AudioClip, convert_pcm_float: bool = True)
10439
return {f"{audio.m_Name}.wav": audio_data}
10540
elif magic[4:8] == b"ftyp":
10641
return {f"{audio.m_Name}.m4a": audio_data}
107-
return dump_samples(audio, audio_data, convert_pcm_float)
108-
109-
110-
SYSTEM_INSTANCES = {} # (channels, flags) -> (pyfmodex_system_instance, lock)
111-
SYSTEM_GLOBAL_LOCK = Lock()
112-
113-
114-
def get_pyfmodex_system_instance(channels: int, flags: int):
115-
global pyfmodex, SYSTEM_INSTANCES, SYSTEM_GLOBAL_LOCK
116-
with SYSTEM_GLOBAL_LOCK:
117-
instance_key = (channels, flags)
118-
if instance_key in SYSTEM_INSTANCES:
119-
return SYSTEM_INSTANCES[instance_key]
120-
121-
system = pyfmodex.System()
122-
system.init(channels, flags, None)
123-
lock = Lock()
124-
SYSTEM_INSTANCES[instance_key] = (system, lock)
125-
return system, lock
126-
127-
128-
def dump_samples(clip: AudioClip, audio_data: bytes, convert_pcm_float: bool = True) -> Dict[str, bytes]:
129-
global pyfmodex
130-
if pyfmodex is None:
131-
import_pyfmodex()
132-
if not pyfmodex:
133-
return {}
134-
135-
if clip.m_Channels is None:
136-
raise NotImplementedError("AudioClip.m_Channels is None, no solution implemented yet.")
137-
138-
system, lock = get_pyfmodex_system_instance(clip.m_Channels, pyfmodex.flags.INIT_FLAGS.NORMAL)
139-
with lock:
140-
sound = system.create_sound(
141-
bytes(audio_data),
142-
pyfmodex.flags.MODE.OPENMEMORY,
143-
exinfo=pyfmodex.structure_declarations.CREATESOUNDEXINFO(
144-
length=len(audio_data),
145-
numchannels=clip.m_Channels,
146-
defaultfrequency=clip.m_Frequency,
147-
),
148-
)
149-
150-
# iterate over subsounds
151-
samples = {}
152-
for i in range(sound.num_subsounds):
153-
if i > 0:
154-
filename = "%s-%i.wav" % (clip.m_Name, i)
155-
else:
156-
filename = "%s.wav" % clip.m_Name
157-
subsound = sound.get_subsound(i)
158-
samples[filename] = subsound_to_wav(subsound, convert_pcm_float)
159-
subsound.release()
160-
161-
sound.release()
162-
return samples
163-
164-
165-
def subsound_to_wav(subsound, convert_pcm_float: bool = True) -> bytes:
166-
# get sound settings
167-
sound_format = subsound.format.format
168-
sound_data_length = subsound.get_length(pyfmodex.enums.TIMEUNIT.PCMBYTES)
169-
channels = subsound.format.channels
170-
bits = subsound.format.bits
171-
sample_rate = int(subsound.default_frequency)
172-
173-
if sound_format in [
174-
pyfmodex.enums.SOUND_FORMAT.PCM8,
175-
pyfmodex.enums.SOUND_FORMAT.PCM16,
176-
pyfmodex.enums.SOUND_FORMAT.PCM24,
177-
pyfmodex.enums.SOUND_FORMAT.PCM32,
178-
]:
179-
# format is PCM integer
180-
audio_format = 1
181-
wav_data_length = sound_data_length
182-
convert_pcm_float = False
183-
elif sound_format == pyfmodex.enums.SOUND_FORMAT.PCMFLOAT:
184-
# format is IEEE 754 float
185-
if convert_pcm_float:
186-
audio_format = 1
187-
bits = 16
188-
wav_data_length = sound_data_length // 2
189-
else:
190-
audio_format = 3
191-
wav_data_length = sound_data_length
192-
else:
193-
raise NotImplementedError("Sound format " + sound_format + " is not supported.")
194-
195-
w = EndianBinaryWriter(endian="<")
196-
197-
# RIFF header
198-
w.write(b"RIFF") # chunk id
199-
w.write_int(wav_data_length + 36) # chunk size - 4 + (8 + 16 (sub chunk 1 size)) + (8 + length (sub chunk 2 size))
200-
w.write(b"WAVE") # format
201-
202-
# fmt chunk - sub chunk 1
203-
w.write(b"fmt ") # sub chunk 1 id
204-
w.write_int(16) # sub chunk 1 size, 16 for PCM
205-
w.write_short(audio_format) # audio format, 1: PCM integer, 3: IEEE 754 float
206-
w.write_short(channels) # number of channels
207-
w.write_int(sample_rate) # sample rate
208-
w.write_int(sample_rate * channels * bits // 8) # byte rate
209-
w.write_short(channels * bits // 8) # block align
210-
w.write_short(bits) # bits per sample
211-
212-
# data chunk - sub chunk 2
213-
w.write(b"data") # sub chunk 2 id
214-
w.write_int(wav_data_length) # sub chunk 2 size
215-
# sub chunk 2 data
216-
lock = subsound.lock(0, sound_data_length)
217-
for ptr, sound_data_length in lock:
218-
ptr_data = ctypes.string_at(ptr, sound_data_length.value)
219-
if convert_pcm_float:
220-
if np is not None:
221-
ptr_data = np.frombuffer(ptr_data, dtype=np.float32)
222-
ptr_data = (ptr_data * (1 << 15)).astype(np.int16).tobytes()
223-
else:
224-
ptr_data = struct.unpack("<%df" % (len(ptr_data) // 4), ptr_data)
225-
ptr_data = struct.pack("<%dh" % len(ptr_data), *[int(f * (1 << 15)) for f in ptr_data])
226-
227-
w.write(ptr_data)
228-
subsound.unlock(*lock)
22942

230-
return w.bytes
43+
return fmod_toolkit.raw_to_wav(
44+
audio_data,
45+
audio.m_Name,
46+
audio.m_Channels or 2,
47+
audio.m_Frequency or 44100,
48+
convert_pcm_float=convert_pcm_float,
49+
)

UnityPy/lib/FMOD/Darwin/__init__.py

Whitespace-only changes.
-2.32 MB
Binary file not shown.

UnityPy/lib/FMOD/Linux/__init__.py

Whitespace-only changes.

UnityPy/lib/FMOD/Linux/arm/__init__.py

Whitespace-only changes.
-1.16 MB
Binary file not shown.

UnityPy/lib/FMOD/Linux/arm64/__init__.py

Whitespace-only changes.
-1.2 MB
Binary file not shown.

UnityPy/lib/FMOD/Linux/x86/__init__.py

Whitespace-only changes.
-1.55 MB
Binary file not shown.

0 commit comments

Comments
 (0)