diff --git a/korlib/CMakeLists.txt b/korlib/CMakeLists.txt index 57e16fe..265ea13 100644 --- a/korlib/CMakeLists.txt +++ b/korlib/CMakeLists.txt @@ -1,6 +1,8 @@ project(korlib) cmake_minimum_required(VERSION 3.0) +set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") + # Stolen shamelessly from PyHSPlasma find_package(PythonLibs REQUIRED) find_package(PythonInterp "${PYTHONLIBS_VERSION_STRING}" REQUIRED) @@ -10,24 +12,30 @@ if (NOT "${PYTHONLIBS_VERSION_STRING}" STREQUAL "${PYTHON_VERSION_STRING}") endif() find_package(HSPlasma REQUIRED) +find_package(Ogg REQUIRED) find_package(OpenGL REQUIRED) +find_package(Vorbis REQUIRED) # Da files set(korlib_HEADERS buffer.h korlib.h + sound.h texture.h ) set(korlib_SOURCES buffer.cpp module.cpp + sound.cpp texture.cpp ) include_directories(${HSPlasma_INCLUDE_DIRS}) +include_directories(${Ogg_INCLUDE_DIR}) include_directories(${OPENGL_INCLUDE_DIR}) include_directories(${PYTHON_INCLUDE_DIRS}) +include_directories(${Vorbis_INCLUDE_DIR}) add_library(_korlib SHARED ${korlib_HEADERS} ${korlib_SOURCES}) if(NOT WIN32) @@ -35,4 +43,4 @@ if(NOT WIN32) else() set_target_properties(_korlib PROPERTIES SUFFIX ".pyd") endif() -target_link_libraries(_korlib HSPlasma ${OPENGL_LIBRARIES} ${PYTHON_LIBRARIES}) +target_link_libraries(_korlib HSPlasma ${Ogg_LIBRARIES} ${OPENGL_LIBRARIES} ${PYTHON_LIBRARIES} ${Vorbis_LIBRARIES}) diff --git a/korlib/cmake/FindOgg.cmake b/korlib/cmake/FindOgg.cmake new file mode 100644 index 0000000..f3071cd --- /dev/null +++ b/korlib/cmake/FindOgg.cmake @@ -0,0 +1,30 @@ +if(Ogg_INCLUDE_DIR AND Ogg_LIBRARY) + set(Ogg_FIND_QUIETLY TRUE) +endif() + + +find_path(Ogg_INCLUDE_DIR ogg/ogg.h + /usr/local/include + /usr/include +) + +find_library(Ogg_LIBRARY NAMES ogg + PATHS /usr/local/lib /usr/lib +) + +set(Ogg_LIBRARIES ${Ogg_LIBRARY}) + + +if(Ogg_INCLUDE_DIR AND Ogg_LIBRARY) + set(Ogg_FOUND TRUE) +endif() + +if (Ogg_FOUND) + if(NOT Ogg_FIND_QUIETLY) + message(STATUS "Found libogg: ${Ogg_INCLUDE_DIR}") + endif() +else() + if(Ogg_FIND_REQUIRED) + message(FATAL_ERROR "Could not find libogg") + endif() +endif() diff --git a/korlib/cmake/FindVorbis.cmake b/korlib/cmake/FindVorbis.cmake new file mode 100644 index 0000000..428d356 --- /dev/null +++ b/korlib/cmake/FindVorbis.cmake @@ -0,0 +1,37 @@ +if(Vorbis_INCLUDE_DIR AND Vorbis_LIBRARY) + set(Vorbis_FIND_QUIETLY TRUE) +endif() + + +find_path(Vorbis_INCLUDE_DIR vorbis/codec.h + /usr/local/include + /usr/include +) + +find_library(Vorbis_LIBRARY NAMES vorbis + PATHS /usr/local/lib /usr/lib +) + +find_library(VorbisFile_LIBRARY NAMES vorbisfile + PATHS /usr/local/lib /usr/lib +) + +set(Vorbis_LIBRARIES + ${Vorbis_LIBRARY} + ${VorbisFile_LIBRARY} +) + + +if(Vorbis_INCLUDE_DIR AND Vorbis_LIBRARY AND VorbisFile_LIBRARY) + set(Vorbis_FOUND TRUE) +endif() + +if (Vorbis_FOUND) + if(NOT Vorbis_FIND_QUIETLY) + message(STATUS "Found libvorbis: ${Vorbis_INCLUDE_DIR}") + endif() +else() + if(Vorbis_FIND_REQUIRED) + message(FATAL_ERROR "Could not find libvorbis") + endif() +endif() diff --git a/korlib/korlib.h b/korlib/korlib.h index ffbc0b6..b59e4a2 100644 --- a/korlib/korlib.h +++ b/korlib/korlib.h @@ -17,6 +17,8 @@ #ifndef _KORLIB_H #define _KORLIB_H +#define NOMINMAX + #include #include diff --git a/korlib/module.cpp b/korlib/module.cpp index 3c5697d..6ff987c 100644 --- a/korlib/module.cpp +++ b/korlib/module.cpp @@ -15,16 +15,23 @@ */ #include "buffer.h" +#include "sound.h" #include "texture.h" extern "C" { +static PyMethodDef korlib_Methods[] = { + { _pycs("inspect_vorbisfile"), (PyCFunction)inspect_vorbisfile, METH_VARARGS, NULL }, + + { NULL, NULL, 0, NULL }, +}; + static PyModuleDef korlib_Module = { PyModuleDef_HEAD_INIT, /* m_base */ "_korlib", /* m_name */ "C++ korlib implementation",/* m_doc */ 0, /* m_size */ - NULL, /* m_methods */ + korlib_Methods, /* m_methods */ NULL, /* m_reload */ NULL, /* m_traverse */ NULL, /* m_clear */ diff --git a/korlib/sound.cpp b/korlib/sound.cpp new file mode 100644 index 0000000..78fff01 --- /dev/null +++ b/korlib/sound.cpp @@ -0,0 +1,119 @@ +/* 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 . + */ + +#include "sound.h" + +#include +#include +#include + +static const int BITS_PER_SAMPLE = 16; + +extern "C" { + +typedef struct { + PyObject_HEAD + hsStream* fThis; + bool fPyOwned; +} pyStream; + +typedef struct { + PyObject_HEAD + plWAVHeader* fThis; + bool fPyOwned; +} pyWAVHeader; + +static size_t _read_stream(void* ptr, size_t size, size_t nmemb, void* datasource) { + hsStream* s = static_cast(datasource); + // hsStream is a bit overzealous protecting us against overreads, so we need to + // make sure to not tick it off. + size_t request = nmemb * size; + size_t remaining = s->size() - s->pos(); + size_t min = std::min(request, remaining); + if (min == 0) { + return 0; + } else { + size_t read = s->read(min, ptr); + return read / size; + } +} + +static int _seek_stream(void* datasource, ogg_int64_t offset, int whence) { + hsStream* s = static_cast(datasource); + switch (whence) { + case SEEK_CUR: + s->skip(offset); + break; + case SEEK_SET: + s->seek(offset); + break; + case SEEK_END: + s->seek(s->size() - offset); + break; + } + return 0; +} + +static long _tell_stream(void* datasource) { + hsStream* s = static_cast(datasource); + return s->pos(); +} + +static ov_callbacks s_callbacks = { + (size_t(*)(void *, size_t, size_t, void *))_read_stream, + (int (*)(void *, ogg_int64_t, int))_seek_stream, + (int (*)(void *))NULL, + (long (*)(void *))_tell_stream, +}; + +PyObject* inspect_vorbisfile(PyObject*, PyObject* args) { + pyStream* stream; + pyWAVHeader* header; + if (!PyArg_ParseTuple(args, "OO", &stream, &header)) { + PyErr_SetString(PyExc_TypeError, "inspect_vorbisfile expects an hsStream, plWAVHeader"); + return NULL; + } + + // The OGG file may actually be in Blender's memory, so we will use hsStream. Therefore, + // we must tell vorbisfile how to do this. + OggVorbis_File vorbis; + int result = ov_open_callbacks(stream->fThis, &vorbis, NULL, 0, s_callbacks); + if (result < 0) { + PyErr_Format(PyExc_RuntimeError, "vorbisfile ov_open_callbacks: %d", result); + return NULL; + } + + header->fThis->setFormatTag(plWAVHeader::kPCMFormatTag); + header->fThis->setBitsPerSample(BITS_PER_SAMPLE); + vorbis_info* info = ov_info(&vorbis, -1); + header->fThis->setNumChannels(info->channels); + header->fThis->setNumSamplesPerSec(info->rate); + unsigned short align = (BITS_PER_SAMPLE * info->channels) >> 3; + header->fThis->setBlockAlign(align); + header->fThis->setAvgBytesPerSec(info->rate * align); + + // This behavior was copied and pasted from CWE + ogg_int64_t size = (ov_pcm_total(&vorbis, -1) - 1) * align; + + // Cleanup + ov_clear(&vorbis); + + // We got the plWAVHeader from Python because we don't link against PyHSPlasma, only libHSPlasma + // Therefore, we only need to return the size. + return PyLong_FromSsize_t(size); +} + +}; diff --git a/korlib/sound.h b/korlib/sound.h new file mode 100644 index 0000000..c67eb1e --- /dev/null +++ b/korlib/sound.h @@ -0,0 +1,28 @@ +/* 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 . + */ + +#ifndef _KORLIB_SOUND_H +#define _KORLIB_SOUND_H + +#include "korlib.h" + +extern "C" { + +PyObject* inspect_vorbisfile(PyObject*, PyObject* args); + +}; + +#endif // _KORLIB_SOUND_H diff --git a/korman/korlib/__init__.py b/korman/korlib/__init__.py index a2c2644..4c3fbe6 100644 --- a/korman/korlib/__init__.py +++ b/korman/korlib/__init__.py @@ -17,3 +17,20 @@ try: from _korlib import * except ImportError: from .texture import * + + def inspect_voribsfile(stream, header): + raise NotImplementedError("Ogg Vorbis not supported unless _korlib is compiled") + + def inspect_wavefile(stream, header): + assert stream.read(4) == b"RIFF" + stream.readInt() + assert stream.read(4) == b"WAVE" + assert stream.read(3) == b"fmt" + header.read(stream) + + # read thru the chunks until we find "data" + while stream.read(4) != b"data" and not stream.eof(): + stream.skip(stream.readInt()) + assert not stream.eof() + size = stream.readInt() + return (header, size) diff --git a/korman/properties/modifiers/sound.py b/korman/properties/modifiers/sound.py index b315f91..57b7d6b 100644 --- a/korman/properties/modifiers/sound.py +++ b/korman/properties/modifiers/sound.py @@ -16,14 +16,310 @@ import bpy from bpy.props import * from bpy.app.handlers import persistent +import math +from pathlib import Path from PyHSPlasma import * +from ... import korlib from .base import PlasmaModifierProperties from ...exporter import ExportError +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(bpy.types.PropertyGroup): + def _sound_picked(self, context): + if not self.sound_data: + 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(context) + + def _update_name(self, context): + if self.is_stereo and self.channel != {"L", "R"}: + self.name = "{}:{}".format(self.sound_data, "L" if "L" in self.channel else "R") + else: + self.name = self.sound_data + enabled = BoolProperty(name="Enabled", default=True, options=set()) - sound_data = StringProperty(name="Sound", description="Sound Datablock", options=set()) + sound_data = StringProperty(name="Sound", description="Sound Datablock", + options=set(), update=_sound_picked) + + is_stereo = BoolProperty(default=True, options={"HIDDEN"}) + is_valid = BoolProperty(default=False, options={"HIDDEN"}) + + soft_region = StringProperty(name="Soft Volume", + description="Soft region this sound can be heard in", + options=set()) + + 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()) + + 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=set(), + subtype="PERCENTAGE") + + fade_in = PointerProperty(type=PlasmaSfxFade, options=set()) + fade_out = PointerProperty(type=PlasmaSfxFade, options=set()) + + @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 + + # There is some bug in the MOUL code that causes a crash if this does not match the expected + # result. There's no sense in debugging that though--the user should never specify + # streaming vs static. That's an implementation detail. + 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_data) + else: + name = "Sfx-{}_{}:{}".format(so.key.name, self.sound_data, channel) + print(" [{}] {}".format(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.soft_region: + sv_bo = bpy.data.objects.get(self.soft_region, None) + if sv_bo is None: + raise ExportError("'{}': Invalid object '{}' for SoundEmit '{}' soft volume".format(self.id_data.name, self.soft_region, self.sound_data)) + sv_mod = sv_bo.plasma_modifiers.softvolume + if not sv_mod.enabled: + raise ExportError("'{}': SoundEmit '{}', '{}' is not a SoftVolume".format(self.id_data.name, self.sound_data, self.soft_region)) + 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 + 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(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(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) + 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 and have the .ogg extension + filename = Path(self._sound.filepath).with_suffix(".ogg").name + 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 + + @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): + raise ExportError("SoundEmitter '{}': Sound '{}' {}".format(self.id_data.name, self.sound_data, msg)) + + @property + def _sound(self): + try: + sound = bpy.data.sounds.get(self.sound_data) + except: + self._raise_error("is not loaded") + else: + return sound class PlasmaSoundEmitter(PlasmaModifierProperties): @@ -38,7 +334,15 @@ class PlasmaSoundEmitter(PlasmaModifierProperties): active_sound_index = IntProperty(options={"HIDDEN"}) def export(self, exporter, bo, so): - pass + 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 self.sounds: + if i.sound_data and i.enabled: + i.convert_sound(exporter, so, winaud) @classmethod def register(cls): @@ -55,10 +359,11 @@ def _toss_orphaned_sounds(scene): for i in bpy.data.objects: soundemit = i.plasma_modifiers.soundemit used_sounds.update((j.sound_data for j in soundemit.sounds)) - for i in bpy.data.sounds: - if i.plasma_owned and i.name not in used_sounds: - i.use_fake_user = False - i.user_clear() + dead_sounds = [i for i in bpy.data.sounds if i.plasma_owned and i.name not in used_sounds] + for i in dead_sounds: + i.use_fake_user = False + i.user_clear() + bpy.data.sounds.remove(i) # collects orphaned Plasma owned sound datablocks bpy.app.handlers.save_pre.append(_toss_orphaned_sounds) diff --git a/korman/ui/modifiers/sound.py b/korman/ui/modifiers/sound.py index feabe39..becabc9 100644 --- a/korman/ui/modifiers/sound.py +++ b/korman/ui/modifiers/sound.py @@ -15,12 +15,17 @@ import bpy +def _draw_fade_ui(modifier, layout, label): + layout.label(label) + layout.prop(modifier, "fade_type", text="") + layout.prop(modifier, "length") + class SoundListUI(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): if item.sound_data: sound = bpy.data.sounds.get(item.sound_data) icon = "SOUND" if sound is not None else "ERROR" - layout.prop(item, "sound_data", emboss=False, icon=icon, text="") + layout.prop(item, "name", emboss=False, icon=icon, text="") layout.prop(item, "enabled", text="") else: layout.label("[Empty]") @@ -58,3 +63,43 @@ def soundemit(modifier, layout, context): row.operator("sound.plasma_pack", icon="UGLYPACKAGE", text="") else: row.operator_menu_enum("sound.plasma_unpack", "method", icon="PACKAGE", text="") + + # If an invalid sound data block is spec'd, let them know about it. + if sound.sound_data and not sound.is_valid: + layout.label(text="Invalid sound specified", icon="ERROR") + + # Core Props + row = layout.row() + row.prop(sound, "sfx_type", text="") + row.prop_menu_enum(sound, "channel") + + split = layout.split() + col = split.column() + col.label("Playback:") + col.prop(sound, "auto_start") + col.prop(sound, "incidental") + col.prop(sound, "loop") + + col.separator() + _draw_fade_ui(sound.fade_in, col, "Fade In:") + col.separator() + _draw_fade_ui(sound.fade_out, col, "Fade Out:") + + col = split.column() + col.label("Cone Effect:") + col.prop(sound, "inner_cone") + col.prop(sound, "outer_cone") + col.prop(sound, "outside_volume", text="Volume") + + col.separator() + col.label("Volume Falloff:") + col.prop(sound, "min_falloff", text="Begin") + col.prop(sound, "max_falloff", text="End") + col.prop(sound, "volume", text="Max Volume") + + # Only allow SoftVolume spec if this is not an FX and this object is not an SV itself + sv = modifier.id_data.plasma_modifiers.softvolume + if not sv.enabled: + col.separator() + col.label("Soft Region:") + col.prop_search(sound, "soft_region", bpy.data, "objects", text="")