You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

708 lines
33 KiB

# This file is part of Korman.
#
# Korman is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Korman 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
from bpy.props import *
from bpy.app.handlers import persistent
from contextlib import contextmanager
import math
from PyHSPlasma import *
from typing import *
from ... import korlib
from .base import PlasmaModifierProperties
from .physics import surface_types
from ...exporter import ExportError
from ...helpers import copy_action, copy_object, GoodNeighbor, TemporaryCollectionItem
from ... import idprops
_randomsound_modes = {
"normal": plRandomSoundMod.kNormal,
"norepeat": plRandomSoundMod.kNoRepeats,
"coverall": plRandomSoundMod.kCoverall | plRandomSoundMod.kNoRepeats,
"sequential": plRandomSoundMod.kSequential
}
class PlasmaRandomSound(PlasmaModifierProperties):
pl_id = "random_sound"
pl_depends = {"soundemit"}
bl_category = "Logic"
bl_label = "Random Sound"
bl_description = ""
mode = EnumProperty(name="Mode",
description="Playback Type",
items=[("random", "Random Time", "Plays a random sound from the emitter at a random time"),
("collision", "Collision Surface", "Plays a random sound when the object's parent collides")],
default="random",
options=set())
# Physical (read: collision) sounds
play_on = EnumProperty(name="Play On",
description="Play sounds on this collision event",
items=[("slide", "Slide", "Plays a random sound on object slide"),
("impact", "Impact", "Plays a random sound on object slide")],
options=set())
surfaces = EnumProperty(name="Play Against",
description="Sounds are played on collision against these surfaces",
items=surface_types[1:],
options={"ENUM_FLAG"})
# Timed random sounds
auto_start = BoolProperty(name="Auto Start",
description="Start playing when the Age loads",
default=True,
options=set())
play_mode = EnumProperty(name="Play Mode",
description="",
items=[("normal", "Any", "Plays any attached sound"),
("norepeat", "No Repeats", "Do not replay a sound immediately after itself"),
("coverall", "Full Set", "Once a sound is played, do not replay it until after all sounds are played"),
("sequential", "Sequential", "Play sounds in the order they appear in the emitter")],
default="norepeat",
options=set())
stop_after_set = BoolProperty(name="Stop After Set",
description="Stop playing after all sounds are played",
default=False,
options=set())
stop_after_play = BoolProperty(name="Stop After Play",
description="Stop playing after one sound is played",
default=False,
options=set())
min_delay = FloatProperty(name="Min Delay",
description="Minimum delay length",
min=0.0,
subtype="TIME", unit="TIME",
options=set())
max_delay = FloatProperty(name="Max Delay",
description="Maximum delay length",
min=0.0,
subtype="TIME", unit="TIME",
options=set())
def export(self, exporter, bo, so):
rndmod = exporter.mgr.find_create_object(plRandomSoundMod, bl=bo, so=so)
if self.mode == "random":
if not self.auto_start:
rndmod.state = plRandomSoundMod.kStopped
if self.stop_after_play:
rndmod.mode |= plRandomSoundMod.kOneCmd
else:
rndmod.minDelay = min(self.min_delay, self.max_delay)
rndmod.maxDelay = max(self.min_delay, self.max_delay)
# Delaying from the start makes ZERO sense. Screw that.
rndmod.mode |= plRandomSoundMod.kDelayFromEnd
rndmod.mode |= _randomsound_modes[self.play_mode]
if self.stop_after_set:
rndmod.mode |= plRandomSoundMod.kOneCycle
elif self.mode == "collision":
rndmod.mode = plRandomSoundMod.kNoRepeats | plRandomSoundMod.kOneCmd
rndmod.state = plRandomSoundMod.kStopped
else:
raise RuntimeError()
def post_export(self, exporter, bo, so):
if self.mode == "collision" and self.surfaces:
parent_bo = bo.parent
if parent_bo is None:
raise ExportError("[{}]: Collision sound objects MUST be parented directly to the collider object.", bo.name)
phys = exporter.mgr.find_object(plGenericPhysical, bl=parent_bo)
if phys is None:
raise ExportError("[{}]: Collision sound objects MUST be parented directly to the collider object.", bo.name)
# The soundGroup on the physical may or may not be the generic "this is my surface type"
# soundGroup with no actual sounds attached. So, we need to lookup the actual one.
sndgroup = exporter.mgr.find_create_object(plPhysicalSndGroup, bl=parent_bo)
sndgroup.group = getattr(plPhysicalSndGroup, parent_bo.plasma_modifiers.collision.surface)
phys.soundGroup = sndgroup.key
rndmod = exporter.mgr.find_key(plRandomSoundMod, bl=bo, so=so)
if self.play_on == "slide":
groupattr = "slideSounds"
elif self.play_on == "impact":
groupattr = "impactSounds"
else:
raise RuntimeError()
sounds = { i: sound for i, sound in enumerate(getattr(sndgroup, groupattr)) }
for surface_name in self.surfaces:
surface_id = getattr(plPhysicalSndGroup, surface_name)
if surface_id in sounds:
exporter.report.warn("Overwriting physical {} surface '{}' ID:{}",
groupattr, surface_name, surface_id)
else:
exporter.report.msg("Got physical {} surface '{}' ID:{}",
groupattr, surface_name, surface_id)
sounds[surface_id] = rndmod
# Keeps the LUT (or should that be lookup vector?) as small as possible
setattr(sndgroup, groupattr, [sounds.get(i) for i in range(max(sounds.keys()) + 1)])
class PlasmaSfxFade(bpy.types.PropertyGroup):
fade_type = EnumProperty(name="Type",
description="Fade Type",
items=[("NONE", "[Disable]", "Don't fade"),
("kLinear", "Linear", "Linear fade"),
("kLogarithmic", "Logarithmic", "Log fade"),
("kExponential", "Exponential", "Exponential fade")],
options=set())
length = FloatProperty(name="Length",
description="Seconds to spend fading",
default=1.0, min=0.0,
options=set(), subtype="TIME", unit="TIME")
class PlasmaSound(idprops.IDPropMixin, bpy.types.PropertyGroup):
@contextmanager
def _lock_sound(self):
exclusive = not self.updating_sound
self.updating_sound = True
try:
yield exclusive
finally:
if exclusive:
self.updating_sound = False
def _update_sound(self, value):
with self._lock_sound() as exclusive:
if exclusive:
if not value:
self.name = "[Empty]"
return
try:
header, size = self._get_sound_info()
except Exception as e:
self.is_valid = False
# this might be perfectly acceptable... who knows?
# user consumable error report to be handled by the UI code
print("---Invalid SFX selection---\n{}\n------".format(str(e)))
else:
self.is_valid = True
self.is_stereo = header.numChannels == 2
self._update_name()
def _update_name(self, context=None):
if self.is_stereo and self.channel != {"L", "R"}:
self.name = "{}:{}".format(self._sound_name, "L" if "L" in self.channel else "R")
else:
self.name = self._sound_name
enabled = BoolProperty(name="Enabled", default=True, options=set())
sound = PointerProperty(name="Sound",
description="Sound Datablock",
type=bpy.types.Sound,
update=_update_sound)
updating_sound = BoolProperty(default=False,
options={"HIDDEN", "SKIP_SAVE"})
is_stereo = BoolProperty(default=True, options={"HIDDEN"})
is_valid = BoolProperty(default=False, options={"HIDDEN"})
sfx_region = PointerProperty(name="Soft Volume",
description="Soft region this sound can be heard in",
type=bpy.types.Object,
poll=idprops.poll_softvolume_objects)
sfx_type = EnumProperty(name="Category",
description="Describes the purpose of this sound",
items=[("kSoundFX", "3D", "3D Positional SoundFX"),
("kAmbience", "Ambience", "Ambient Sounds"),
("kBackgroundMusic", "Music", "Background Music"),
("kGUISound", "GUI", "GUI Effect"),
("kNPCVoices", "NPC", "NPC Speech")],
options=set())
channel = EnumProperty(name="Channel",
description="Which channel(s) to play",
items=[("L", "Left", "Left Channel"),
("R", "Right", "Right Channel")],
options={"ENUM_FLAG"},
default={"L", "R"},
update=_update_name)
auto_start = BoolProperty(name="Auto Start",
description="Start playing when the age is loaded",
default=False,
options=set())
incidental = BoolProperty(name="Incidental",
description="Sound is a low-priority incident and the engine may forgo playback",
default=False,
options=set())
loop = BoolProperty(name="Loop",
description="Loop the sound",
default=False,
options=set())
local_only = BoolProperty(name="Local Only",
description="Sounds only plays for local avatar",
default=False,
options=set())
inner_cone = FloatProperty(name="Inner Angle",
description="Angle of the inner cone from the negative Z-axis",
min=0, max=math.radians(360), default=0, step=100,
options=set(),
subtype="ANGLE")
outer_cone = FloatProperty(name="Outer Angle",
description="Angle of the outer cone from the negative Z-axis",
min=0, max=math.radians(360), default=math.radians(360), step=100,
options=set(),
subtype="ANGLE")
outside_volume = IntProperty(name="Outside Volume",
description="Sound's volume when outside the outer cone",
min=0, max=100, default=100,
options=set(),
subtype="PERCENTAGE")
min_falloff = IntProperty(name="Begin Falloff",
description="Distance where volume attenuation begins",
min=0, max=1000000000, default=1,
options=set(),
subtype="DISTANCE")
max_falloff = IntProperty(name="End Falloff",
description="Distance where the sound is inaudible",
min=0, max=1000000000, default=1000,
options=set(),
subtype="DISTANCE")
volume = IntProperty(name="Volume",
description="Volume to play the sound",
min=0, max=100, default=100,
options={"ANIMATABLE"},
subtype="PERCENTAGE")
fade_in = PointerProperty(type=PlasmaSfxFade, options=set())
fade_out = PointerProperty(type=PlasmaSfxFade, options=set())
def _get_package_value(self):
if self.sound is not None:
self.package_value = self.sound.plasma_sound.package
return self.package_value
def _set_package_value(self, value):
if self.sound is not None:
self.sound.plasma_sound.package = value
# This is really a property of the sound itself, not of this particular emitter instance.
# However, to prevent weird UI inconsistencies where the button might be missing or change
# states when clearing the sound pointer, we'll cache the actual value here.
package = BoolProperty(name="Export",
description="Package this file in the age export",
get=_get_package_value, set=_set_package_value,
options=set())
package_value = BoolProperty(options={"HIDDEN", "SKIP_SAVE"})
@property
def channel_override(self):
if self.is_stereo and len(self.channel) == 1:
return min(self.channel)
else:
return None
def convert_sound(self, exporter, so, audible):
header, dataSize = self._get_sound_info()
length = dataSize / header.avgBytesPerSec
# HAX: Ensure that the sound file is copied to game, if applicable.
if self._sound.plasma_sound.package:
exporter.output.add_sfx(self._sound)
# There is some bug in the MOUL code that causes a crash if this does not match the expected
# result. Worse, PotS seems to not like static sounds that are brand-new to it. Possibly because
# it needs to be decompressed outside of game. There's no sense in debugging any of that
# though--the user should never specify streaming vs static. That's an implementation detail.
if exporter.mgr.getVer() != pvMoul and self._sound.plasma_sound.package:
pClass = plWin32StreamingSound
else:
pClass = plWin32StreamingSound if length > 4.0 else plWin32StaticSound
# OK. Any Plasma engine that uses OpenAL (MOUL) is subject to this restriction.
# 3D Positional audio MUST... and I mean MUST... have mono emitters.
# That means if the user has specified 3D and a stereo sound AND both channels, we MUST
# export two emitters from here. Otherwise, it's no biggie. Wheeeeeeeeeeeeeeeeeeeeeeeee
if self.is_3d_stereo or (self.is_stereo and len(self.channel) == 1):
header.avgBytesPerSec = int(header.avgBytesPerSec / 2)
header.numChannels = int(header.numChannels / 2)
header.blockAlign = int(header.blockAlign / 2)
dataSize = int(dataSize / 2)
if self.is_3d_stereo:
audible.addSound(self._convert_sound(exporter, so, pClass, header, dataSize, channel="L"))
audible.addSound(self._convert_sound(exporter, so, pClass, header, dataSize, channel="R"))
else:
audible.addSound(self._convert_sound(exporter, so, pClass, header, dataSize, channel=self.channel_override))
def _convert_sound(self, exporter, so, pClass, wavHeader, dataSize, channel=None):
if channel is None:
name = "Sfx-{}_{}".format(so.key.name, self._sound_name)
else:
name = "Sfx-{}_{}:{}".format(so.key.name, self._sound_name, channel)
exporter.report.msg("[{}] {}", pClass.__name__[2:], name)
sound = exporter.mgr.find_create_object(pClass, so=so, name=name)
# If this object is a soft volume itself, we will use our own soft region.
# Otherwise, check what they specified...
sv_mod, sv_key = self.id_data.plasma_modifiers.softvolume, None
if sv_mod.enabled:
sv_key = sv_mod.get_key(exporter, so)
elif self.sfx_region:
sv_mod = self.sfx_region.plasma_modifiers.softvolume
if not sv_mod.enabled:
raise ExportError("'{}': SoundEmit '{}', '{}' is not a SoftVolume".format(self.id_data.name, self._sound_name, self.sfx_region.name))
sv_key = sv_mod.get_key(exporter)
if sv_key is not None:
sv_key.object.listenState |= plSoftVolume.kListenCheck | plSoftVolume.kListenDirty | plSoftVolume.kListenRegistered
sound.softRegion = sv_key
# Sound
sound.type = getattr(plSound, self.sfx_type)
if sound.type == plSound.kSoundFX:
sound.properties |= plSound.kPropIs3DSound
if self.auto_start:
sound.properties |= plSound.kPropAutoStart
if self.loop:
sound.properties |= plSound.kPropLooping
if self.incidental:
sound.properties |= plSound.kPropIncidental
if self.local_only:
sound.properties |= plSound.kPropLocalOnly
sound.dataBuffer = self._find_sound_buffer(exporter, so, wavHeader, dataSize, channel)
# Cone effect
# I have observed that Blender 2.77's UI doesn't show the appropriate unit (degrees) for
# IntProperty angle subtypes. So, we're storing the angles as floats in Blender even though
# Plasma only wants integers. Sigh.
sound.innerCone = int(math.degrees(self.inner_cone))
sound.outerCone = int(math.degrees(self.outer_cone))
sound.outerVol = self.outside_volume
# Falloff
sound.desiredVolume = self.volume / 100.0
sound.minFalloff = self.min_falloff
sound.maxFalloff = self.max_falloff
# Fade FX
fade_in, fade_out = sound.fadeInParams, sound.fadeOutParams
for blfade, plfade in ((self.fade_in, fade_in), (self.fade_out, fade_out)):
if blfade.fade_type == "NONE":
plfade.lengthInSecs = 0.0
else:
plfade.lengthInSecs = blfade.length
plfade.type = getattr(plSound.plFadeParams, blfade.fade_type)
plfade.currTime = -1.0
# Some manual fiddling -- this is hidden deep inside the 3dsm exporter...
# Kind of neat how it's all generic though :)
fade_in.volStart = 0.0
fade_in.volEnd = 1.0
fade_out.volStart = 1.0
fade_out.volEnd = 0.0
fade_out.stopWhenDone = True
# Some last minute buffer tweaking based on our props here...
buffer = sound.dataBuffer.object
if isinstance(sound, plWin32StreamingSound):
buffer.flags |= plSoundBuffer.kStreamCompressed
if sound.type == plSound.kBackgroundMusic:
buffer.flags |= plSoundBuffer.kAlwaysExternal
# Win32Sound
if channel == "L":
sound.channel = plWin32Sound.kLeftChannel
else:
sound.channel = plWin32Sound.kRightChannel
# Whew, that was a lot of work!
return sound.key
def _get_sound_info(self):
"""Generates a tuple (plWAVHeader, PCMsize) from the current sound"""
sound = self._sound
if sound.packed_file is None:
stream = hsFileStream()
try:
stream.open(bpy.path.abspath(sound.filepath), fmRead)
except IOError:
self._raise_error("failed to open file")
else:
stream = hsRAMStream()
stream.buffer = sound.packed_file.data
try:
magic = stream.read(4)
stream.rewind()
header = plWAVHeader()
if magic == b"RIFF":
size = korlib.inspect_wavefile(stream, header)
return (header, size)
elif magic == b"OggS":
size = korlib.inspect_vorbisfile(stream, header)
return (header, size)
else:
raise NotSupportedError("unsupported audio format")
except Exception as e:
self._raise_error(str(e))
finally:
stream.close()
def _find_sound_buffer(self, exporter, so, wavHeader, dataSize, channel):
# First, cleanup the file path to not have directories
filename = bpy.path.basename(self._sound.filepath)
if channel is None:
key_name = filename
else:
key_name = "{}:{}".format(filename, channel)
key = exporter.mgr.find_key(plSoundBuffer, so=so, name=key_name)
if key is None:
sound = exporter.mgr.add_object(plSoundBuffer, so=so, name=key_name)
sound.header = wavHeader
sound.fileName = filename
sound.dataLength = dataSize
# Maybe someday we will allow packed sounds? I'm in no hurry...
sound.flags |= plSoundBuffer.kIsExternal
if channel == "L":
sound.flags |= plSoundBuffer.kOnlyLeftChannel
elif channel == "R":
sound.flags |= plSoundBuffer.kOnlyRightChannel
key = sound.key
return key
@classmethod
def _idprop_mapping(cls):
return {"sound": "sound_data",
"sfx_region": "soft_region"}
def _idprop_sources(self):
return {"sound_data": bpy.data.sounds,
"soft_region": bpy.data.objects}
@property
def is_3d_stereo(self):
return self.sfx_type == "kSoundFX" and self.channel == {"L", "R"} and self.is_stereo
def _raise_error(self, msg):
if self.sound:
raise ExportError("SoundEmitter '{}': Sound '{}' {}".format(self.id_data.name, self.sound.name, msg))
else:
raise ExportError("SoundEmitter '{}': {}".format(self.id_data.name, msg))
@property
def _sound(self):
if not self.sound:
self._raise_error("has an invalid sound specified")
return self.sound
@property
def _sound_name(self):
if self.sound:
return self.sound.name
return ""
class PlasmaSoundEmitter(PlasmaModifierProperties):
pl_id = "soundemit"
bl_category = "Logic"
bl_label = "Sound Emitter"
bl_description = "Point at which sound(s) are played"
bl_icon = "SPEAKER"
sounds = CollectionProperty(type=PlasmaSound)
active_sound_index = IntProperty(options={"HIDDEN"})
stereize_left = PointerProperty(type=bpy.types.Object, options={"HIDDEN", "SKIP_SAVE"})
stereize_right = PointerProperty(type=bpy.types.Object, options={"HIDDEN", "SKIP_SAVE"})
def sanity_check(self):
modifiers = self.id_data.plasma_modifiers
# Sound emitters can potentially export sounds to more than one emitter SceneObject. Currently,
# this happens for 3D stereo sounds. That means that any modifier that expects for all of
# this emitter's sounds to be attached to this plAudioInterface might have a bad time.
if self.have_3d_stereo and modifiers.random_sound.enabled:
raise ExportError(f"{self.id_data.name}: Random Sound modifier cannot be applied to a Sound Emitter with 3D Stereo sounds.")
def _generate_stereized_emitter(self, exporter, bo: bpy.types.Object, channel: str, attr: str):
# Duplicate the current sound emitter as a non-linked object so that we have all the
# information that the parent emitter has, but we're free to turn off things as needed.
# NOTE: Don't immediately yield the copied object - we need to work on it first. If you immediately
# yield it, we get infinite recursion due to the copied object also having 3D Stereo sounds.
try:
emitter_obj = copy_object(bo, f"{bo.name}_Stereo-Ize:{channel}")
emitter_obj.location = (0.0, 0.0, 0.0)
emitter_obj.parent = bo
# In case some bozo is using a visual mesh as a sound emitter, clear the materials
# off the duplicate to prevent it from being visible in the world.
if emitter_obj.type == "MESH":
emitter_obj.data.materials.clear()
# We want to allow animations and sounds to export from the new emitter.
bad_mods = filter(
lambda x: x.pl_id not in {"animation", "soundemit"},
emitter_obj.plasma_modifiers.modifiers
)
for i in bad_mods:
i.enabled = False
# But only 3D stereo sounds!
soundemit_mod = emitter_obj.plasma_modifiers.soundemit
for sound in soundemit_mod.sounds:
if sound.is_3d_stereo:
sound.channel = set(channel)
else:
sound.enabled = False
# And only sound volume animations!
emitter_obj_action = yield copy_action(emitter_obj)
if emitter_obj_action is not None:
volume_paths = frozenset((i.path_from_id("volume") for i in soundemit_mod.sounds if i.enabled))
toasty_fcurves = [fcurve for fcurve in emitter_obj_action.fcurves if fcurve.data_path not in volume_paths]
for i in reversed(toasty_fcurves):
emitter_obj_action.fcurves.remove(i)
# Again, only sound volume animations, which are handled above.
emitter_data_action = yield copy_action(emitter_obj.data)
if emitter_data_action is not None:
emitter_data_action.fcurves.clear()
except Exception:
bpy.data.objects.remove(emitter_obj)
raise
else:
yield emitter_obj
setattr(self, attr, emitter_obj)
def _find_animation_groups(self, bo: bpy.types.Object):
is_anim_group = lambda x: (
x is not bo and
x.plasma_object.enabled and
x.plasma_modifiers.animation_group.enabled
)
for i in filter(is_anim_group, bpy.data.objects):
group = i.plasma_modifiers.animation_group
for child in group.children:
if child.child_anim == self.id_data:
yield child
def _add_child_animation(self, exporter, group, bo: bpy.types.Object, temporary=False):
if temporary:
child = exporter.exit_stack.enter_context(TemporaryCollectionItem(group.children))
else:
child = group.children.add()
child.child_anim = bo
def pre_export(self, exporter, bo: bpy.types.Object):
# Stereo 3D sounds are a very, very special case. We need to export mono sound sources.
# However, to get Plasma's Stereo-Ize feature to work, they need to be completely separate
# objects that the engine can move around itself. Those need to be duplicates of this
# blender object so that all animation data and whatnot remains.
if self.have_3d_stereo:
toggle = exporter.exit_stack.enter_context(GoodNeighbor())
# Find any animation groups that we're a part of - we need to be a member of those,
# or create one *if* any animations
yield from self._generate_stereized_emitter(exporter, bo, "L", "stereize_left")
yield from self._generate_stereized_emitter(exporter, bo, "R", "stereize_right")
# If some animation data persisted on the new emitters, then we need to make certain
# that those animations are targetted by anyone trying to control us. That's an
# animation group modifier (plMsgForwarder) for anyone playing along at home.
if self.stereize_left.plasma_object.has_animation_data or self.stereize_right.plasma_object.has_animation_data:
my_anim_groups = list(self._find_animation_groups(bo))
# If no one contains this sound emitter, then we need to be an animation group.
if not my_anim_groups:
group = bo.plasma_modifiers.animation_group
if not group.enabled:
toggle.track(group, "enabled", True)
for i in group.children:
toggle.track(i, "enabled", False)
my_anim_groups.append(group)
# Now that we have the animation groups, feed in the generated emitter objects
# as ephemeral child animations. They should be removed from the modifier when the
# export finishes.
for anim_group in my_anim_groups:
self._add_child_animation(exporter, anim_group, self.stereize_left, True)
self._add_child_animation(exporter, anim_group, self.stereize_right, True)
# Temporarily disable the 3D stereo sounds on this emitter during the export - so
# this emitter will export everything that isn't 3D stereo.
for i in filter(lambda x: x.is_3d_stereo, self.sounds):
toggle.track(i, "enabled", False)
def export(self, exporter, bo, so):
if any((i.enabled for i in self.sounds)):
winaud = exporter.mgr.find_create_object(plWinAudible, so=so, name=self.key_name)
winaud.sceneNode = exporter.mgr.get_scene_node(so.key.location)
aiface = exporter.mgr.find_create_object(plAudioInterface, so=so, name=self.key_name)
aiface.audible = winaud.key
# Pass this off to each individual sound for conversion
for i in filter(lambda x: x.enabled, self.sounds):
i.convert_sound(exporter, so, winaud)
# Back to our faked emitters for 3D stereo sounds... Create the stereo-ize object
# that will cause Plasma to move the object around in the 3D world. In the future,
# this should probably be split out to a separate modifier.
if self.stereize_left and self.stereize_right:
self._convert_stereize(exporter, self.stereize_left, "L")
self._convert_stereize(exporter, self.stereize_right, "R")
def post_export(self, exporter, bo: bpy.types.Object, so: plSceneObject):
if self.stereize_left and self.stereize_right:
self._handle_stereize_lfm(exporter, self.stereize_left, so)
self._handle_stereize_lfm(exporter, self.stereize_right, so)
def _convert_stereize(self, exporter, bo: bpy.types.Object, channel: str) -> None:
# TODO: This should probably be moved into a Stereo-Ize modifier of some sort
stereoize = exporter.mgr.find_create_object(plStereizer, bl=bo)
stereoize.setFlag(plStereizer.kLeftChannel, channel == "L")
stereoize.ambientDist = 50.0
stereoize.sepDist = (5.0, 100.0)
stereoize.transition = 25.0
stereoize.tanAng = math.radians(30.0)
def _handle_stereize_lfm(self, exporter, child_bo: bpy.types.Object, parent_so: plSceneObject) -> None:
# TODO: This should probably be moved into a Stereo-Ize modifier of some sort
stereizer = exporter.mgr.find_object(plStereizer, bl=child_bo)
for lfm_key in filter(lambda x: isinstance(x.object, plLineFollowMod), parent_so.modifiers):
stereizer.setFlag(plStereizer.kHasMaster, True)
lfm_key.object.addStereizer(stereizer.key)
def get_sound_keys(self, exporter, name=None, sound=None) -> Iterator[Tuple[plKey, int]]:
assert name or sound
if sound is None:
sound = next((i for i in self.sounds if i._sound_name == name), None)
if sound is None:
raise ValueError(name)
if sound.is_3d_stereo:
yield from self.stereize_left.plasma_modifiers.soundemit.get_sound_keys(exporter, sound.name)
yield from self.stereize_right.plasma_modifiers.soundemit.get_sound_keys(exporter, sound.name)
else:
for i, j in enumerate(filter(lambda x: x.enabled, self.sounds)):
if sound == j:
yield exporter.mgr.find_create_key(plAudioInterface, bl=self.id_data), i
@property
def have_3d_stereo(self) -> bool:
return any((i.is_3d_stereo for i in self.sounds if i.enabled))
@property
def requires_actor(self):
return True