11from __future__ import annotations
22
3- import ctypes
4- import os
5- import platform
6- from threading import Lock
73from typing import TYPE_CHECKING , Dict
84
9- from UnityPy . streams import EndianBinaryWriter
5+ import fmod_toolkit
106
117from ..helpers .ResourceReader import get_resource_data
128
13- try :
14- import numpy as np
15- except ImportError :
16- np = None
17- import struct
18-
199if 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
8213def 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+ )
0 commit comments