Browse Source

Implement SoundEmit modifier

This sound emitter modifier is almost as fully functional as PlasmaMAX's various sound emitter components. Additional functionality was added to C korlib so that artists can specify OGG Vorbis sound files. If korlib is not compiled, only WAVE sounds can be utilized in Korman. This fixes some of the more fiddly bugs related to exporting to CWE that were seen in PyPRP.

Sound nodes to be implemented...
pull/35/head
Adam Johnson 8 years ago
parent
commit
36f0ac194d
  1. 10
      korlib/CMakeLists.txt
  2. 30
      korlib/cmake/FindOgg.cmake
  3. 37
      korlib/cmake/FindVorbis.cmake
  4. 2
      korlib/korlib.h
  5. 9
      korlib/module.cpp
  6. 119
      korlib/sound.cpp
  7. 28
      korlib/sound.h
  8. 17
      korman/korlib/__init__.py
  9. 317
      korman/properties/modifiers/sound.py
  10. 47
      korman/ui/modifiers/sound.py

10
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})

30
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()

37
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()

2
korlib/korlib.h

@ -17,6 +17,8 @@
#ifndef _KORLIB_H
#define _KORLIB_H
#define NOMINMAX
#include <cstdint>
#include <Python.h>

9
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 */

119
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 <http://www.gnu.org/licenses/>.
*/
#include "sound.h"
#include <PRP/Audio/plSoundBuffer.h>
#include <Stream/hsStream.h>
#include <vorbis/vorbisfile.h>
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<hsStream*>(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<hsStream*>(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<hsStream*>(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);
}
};

28
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 <http://www.gnu.org/licenses/>.
*/
#ifndef _KORLIB_SOUND_H
#define _KORLIB_SOUND_H
#include "korlib.h"
extern "C" {
PyObject* inspect_vorbisfile(PyObject*, PyObject* args);
};
#endif // _KORLIB_SOUND_H

17
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)

317
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)

47
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="")

Loading…
Cancel
Save