mirror of
https://github.com/H-uru/korman.git
synced 2025-07-14 02:27:36 -04:00
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...
This commit is contained in:
@ -1,6 +1,8 @@
|
|||||||
project(korlib)
|
project(korlib)
|
||||||
cmake_minimum_required(VERSION 3.0)
|
cmake_minimum_required(VERSION 3.0)
|
||||||
|
|
||||||
|
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
|
||||||
|
|
||||||
# Stolen shamelessly from PyHSPlasma
|
# Stolen shamelessly from PyHSPlasma
|
||||||
find_package(PythonLibs REQUIRED)
|
find_package(PythonLibs REQUIRED)
|
||||||
find_package(PythonInterp "${PYTHONLIBS_VERSION_STRING}" REQUIRED)
|
find_package(PythonInterp "${PYTHONLIBS_VERSION_STRING}" REQUIRED)
|
||||||
@ -10,24 +12,30 @@ if (NOT "${PYTHONLIBS_VERSION_STRING}" STREQUAL "${PYTHON_VERSION_STRING}")
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_package(HSPlasma REQUIRED)
|
find_package(HSPlasma REQUIRED)
|
||||||
|
find_package(Ogg REQUIRED)
|
||||||
find_package(OpenGL REQUIRED)
|
find_package(OpenGL REQUIRED)
|
||||||
|
find_package(Vorbis REQUIRED)
|
||||||
|
|
||||||
# Da files
|
# Da files
|
||||||
set(korlib_HEADERS
|
set(korlib_HEADERS
|
||||||
buffer.h
|
buffer.h
|
||||||
korlib.h
|
korlib.h
|
||||||
|
sound.h
|
||||||
texture.h
|
texture.h
|
||||||
)
|
)
|
||||||
|
|
||||||
set(korlib_SOURCES
|
set(korlib_SOURCES
|
||||||
buffer.cpp
|
buffer.cpp
|
||||||
module.cpp
|
module.cpp
|
||||||
|
sound.cpp
|
||||||
texture.cpp
|
texture.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
include_directories(${HSPlasma_INCLUDE_DIRS})
|
include_directories(${HSPlasma_INCLUDE_DIRS})
|
||||||
|
include_directories(${Ogg_INCLUDE_DIR})
|
||||||
include_directories(${OPENGL_INCLUDE_DIR})
|
include_directories(${OPENGL_INCLUDE_DIR})
|
||||||
include_directories(${PYTHON_INCLUDE_DIRS})
|
include_directories(${PYTHON_INCLUDE_DIRS})
|
||||||
|
include_directories(${Vorbis_INCLUDE_DIR})
|
||||||
|
|
||||||
add_library(_korlib SHARED ${korlib_HEADERS} ${korlib_SOURCES})
|
add_library(_korlib SHARED ${korlib_HEADERS} ${korlib_SOURCES})
|
||||||
if(NOT WIN32)
|
if(NOT WIN32)
|
||||||
@ -35,4 +43,4 @@ if(NOT WIN32)
|
|||||||
else()
|
else()
|
||||||
set_target_properties(_korlib PROPERTIES SUFFIX ".pyd")
|
set_target_properties(_korlib PROPERTIES SUFFIX ".pyd")
|
||||||
endif()
|
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
Normal file
30
korlib/cmake/FindOgg.cmake
Normal file
@ -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
Normal file
37
korlib/cmake/FindVorbis.cmake
Normal file
@ -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()
|
@ -17,6 +17,8 @@
|
|||||||
#ifndef _KORLIB_H
|
#ifndef _KORLIB_H
|
||||||
#define _KORLIB_H
|
#define _KORLIB_H
|
||||||
|
|
||||||
|
#define NOMINMAX
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <Python.h>
|
#include <Python.h>
|
||||||
|
|
||||||
|
@ -15,16 +15,23 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "buffer.h"
|
#include "buffer.h"
|
||||||
|
#include "sound.h"
|
||||||
#include "texture.h"
|
#include "texture.h"
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
|
static PyMethodDef korlib_Methods[] = {
|
||||||
|
{ _pycs("inspect_vorbisfile"), (PyCFunction)inspect_vorbisfile, METH_VARARGS, NULL },
|
||||||
|
|
||||||
|
{ NULL, NULL, 0, NULL },
|
||||||
|
};
|
||||||
|
|
||||||
static PyModuleDef korlib_Module = {
|
static PyModuleDef korlib_Module = {
|
||||||
PyModuleDef_HEAD_INIT, /* m_base */
|
PyModuleDef_HEAD_INIT, /* m_base */
|
||||||
"_korlib", /* m_name */
|
"_korlib", /* m_name */
|
||||||
"C++ korlib implementation",/* m_doc */
|
"C++ korlib implementation",/* m_doc */
|
||||||
0, /* m_size */
|
0, /* m_size */
|
||||||
NULL, /* m_methods */
|
korlib_Methods, /* m_methods */
|
||||||
NULL, /* m_reload */
|
NULL, /* m_reload */
|
||||||
NULL, /* m_traverse */
|
NULL, /* m_traverse */
|
||||||
NULL, /* m_clear */
|
NULL, /* m_clear */
|
||||||
|
119
korlib/sound.cpp
Normal file
119
korlib/sound.cpp
Normal file
@ -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
Normal file
28
korlib/sound.h
Normal file
@ -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,3 +17,20 @@ try:
|
|||||||
from _korlib import *
|
from _korlib import *
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from .texture import *
|
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)
|
||||||
|
@ -16,14 +16,310 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from bpy.props import *
|
from bpy.props import *
|
||||||
from bpy.app.handlers import persistent
|
from bpy.app.handlers import persistent
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
from PyHSPlasma import *
|
from PyHSPlasma import *
|
||||||
|
|
||||||
|
from ... import korlib
|
||||||
from .base import PlasmaModifierProperties
|
from .base import PlasmaModifierProperties
|
||||||
from ...exporter import ExportError
|
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):
|
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())
|
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):
|
class PlasmaSoundEmitter(PlasmaModifierProperties):
|
||||||
@ -38,7 +334,15 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
|
|||||||
active_sound_index = IntProperty(options={"HIDDEN"})
|
active_sound_index = IntProperty(options={"HIDDEN"})
|
||||||
|
|
||||||
def export(self, exporter, bo, so):
|
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
|
@classmethod
|
||||||
def register(cls):
|
def register(cls):
|
||||||
@ -55,10 +359,11 @@ def _toss_orphaned_sounds(scene):
|
|||||||
for i in bpy.data.objects:
|
for i in bpy.data.objects:
|
||||||
soundemit = i.plasma_modifiers.soundemit
|
soundemit = i.plasma_modifiers.soundemit
|
||||||
used_sounds.update((j.sound_data for j in soundemit.sounds))
|
used_sounds.update((j.sound_data for j in soundemit.sounds))
|
||||||
for i in bpy.data.sounds:
|
dead_sounds = [i for i in bpy.data.sounds if i.plasma_owned and i.name not in used_sounds]
|
||||||
if i.plasma_owned and i.name not in used_sounds:
|
for i in dead_sounds:
|
||||||
i.use_fake_user = False
|
i.use_fake_user = False
|
||||||
i.user_clear()
|
i.user_clear()
|
||||||
|
bpy.data.sounds.remove(i)
|
||||||
|
|
||||||
# collects orphaned Plasma owned sound datablocks
|
# collects orphaned Plasma owned sound datablocks
|
||||||
bpy.app.handlers.save_pre.append(_toss_orphaned_sounds)
|
bpy.app.handlers.save_pre.append(_toss_orphaned_sounds)
|
||||||
|
@ -15,12 +15,17 @@
|
|||||||
|
|
||||||
import bpy
|
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):
|
class SoundListUI(bpy.types.UIList):
|
||||||
def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0):
|
def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0):
|
||||||
if item.sound_data:
|
if item.sound_data:
|
||||||
sound = bpy.data.sounds.get(item.sound_data)
|
sound = bpy.data.sounds.get(item.sound_data)
|
||||||
icon = "SOUND" if sound is not None else "ERROR"
|
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="")
|
layout.prop(item, "enabled", text="")
|
||||||
else:
|
else:
|
||||||
layout.label("[Empty]")
|
layout.label("[Empty]")
|
||||||
@ -58,3 +63,43 @@ def soundemit(modifier, layout, context):
|
|||||||
row.operator("sound.plasma_pack", icon="UGLYPACKAGE", text="")
|
row.operator("sound.plasma_pack", icon="UGLYPACKAGE", text="")
|
||||||
else:
|
else:
|
||||||
row.operator_menu_enum("sound.plasma_unpack", "method", icon="PACKAGE", text="")
|
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="")
|
||||||
|
Reference in New Issue
Block a user