mirror of https://github.com/H-uru/korman.git
16 changed files with 974 additions and 9 deletions
@ -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() |
@ -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() |
@ -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); |
||||||
|
} |
||||||
|
|
||||||
|
}; |
@ -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
|
@ -0,0 +1,88 @@ |
|||||||
|
# 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/>. |
||||||
|
|
||||||
|
import bpy |
||||||
|
from bpy.props import * |
||||||
|
|
||||||
|
class SoundOperator: |
||||||
|
@classmethod |
||||||
|
def poll(cls, context): |
||||||
|
return context.scene.render.engine == "PLASMA_GAME" |
||||||
|
|
||||||
|
|
||||||
|
class PlasmaSoundOpenOperator(SoundOperator, bpy.types.Operator): |
||||||
|
bl_idname = "sound.plasma_open" |
||||||
|
bl_label = "Load Sound" |
||||||
|
bl_options = {"INTERNAL"} |
||||||
|
|
||||||
|
filter_glob = StringProperty(default="*.ogg;*.wav", options={"HIDDEN"}) |
||||||
|
filepath = StringProperty(subtype="FILE_PATH") |
||||||
|
|
||||||
|
data_path = StringProperty(options={"HIDDEN"}) |
||||||
|
sound_property = StringProperty(options={"HIDDEN"}) |
||||||
|
|
||||||
|
def execute(self, context): |
||||||
|
# Check to see if the sound exists... Because the sneakily introduced bpy.data.sounds.load |
||||||
|
# check_existing doesn't tell us if it already exists... dammit... |
||||||
|
# We don't want to take ownership forcefully if we don't have to. |
||||||
|
for i in bpy.data.sounds: |
||||||
|
if self.filepath == i.filepath: |
||||||
|
sound = i |
||||||
|
break |
||||||
|
else: |
||||||
|
sound = bpy.data.sounds.load(self.filepath) |
||||||
|
sound.plasma_owned = True |
||||||
|
sound.use_fake_user = True |
||||||
|
|
||||||
|
# Now do the stanky leg^H^H^H^H^H^H^H^H^H^H deed and put the sound on the mod |
||||||
|
dest = eval(self.data_path) |
||||||
|
setattr(dest, self.sound_property, sound.name) |
||||||
|
return {"FINISHED"} |
||||||
|
|
||||||
|
def invoke(self, context, event): |
||||||
|
context.window_manager.fileselect_add(self) |
||||||
|
return {"RUNNING_MODAL"} |
||||||
|
|
||||||
|
|
||||||
|
class PlasmaSoundPackOperator(SoundOperator, bpy.types.Operator): |
||||||
|
bl_idname = "sound.plasma_pack" |
||||||
|
bl_label = "Pack" |
||||||
|
bl_options = {"INTERNAL"} |
||||||
|
|
||||||
|
def execute(self, context): |
||||||
|
soundemit = context.active_object.plasma_modifiers.soundemit |
||||||
|
sound = bpy.data.sounds.get(soundemit.sounds[soundemit.active_sound_index].sound_data) |
||||||
|
sound.pack() |
||||||
|
return {"FINISHED"} |
||||||
|
|
||||||
|
|
||||||
|
class PlasmaSoundUnpackOperator(SoundOperator, bpy.types.Operator): |
||||||
|
bl_idname = "sound.plasma_unpack" |
||||||
|
bl_label = "Unpack" |
||||||
|
bl_options = {"INTERNAL"} |
||||||
|
|
||||||
|
method = EnumProperty(name="Method", description="How to unpack", |
||||||
|
# See blender/makesrna/intern/rna_packedfile.c |
||||||
|
items=[("USE_LOCAL", "Use local file", "", 5), |
||||||
|
("WRITE_LOCAL", "Write Local File (overwrite existing)", "", 4), |
||||||
|
("USE_ORIGINAL", "Use Original File", "", 6), |
||||||
|
("WRITE_ORIGINAL", "Write Original File (overwrite existing)", "", 3)], |
||||||
|
options=set()) |
||||||
|
|
||||||
|
def execute(self, context): |
||||||
|
soundemit = context.active_object.plasma_modifiers.soundemit |
||||||
|
sound = bpy.data.sounds.get(soundemit.sounds[soundemit.active_sound_index].sound_data) |
||||||
|
sound.unpack(self.method) |
||||||
|
return {"FINISHED"} |
@ -0,0 +1,384 @@ |
|||||||
|
# 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/>. |
||||||
|
|
||||||
|
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(), 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): |
||||||
|
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"}) |
||||||
|
|
||||||
|
def export(self, exporter, bo, so): |
||||||
|
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) |
||||||
|
|
||||||
|
def get_sound_indices(self, name): |
||||||
|
"""Returns the index of the given sound in the plWin32Sound. This is needed because stereo |
||||||
|
3D sounds export as two mono sound objects -- wheeeeee""" |
||||||
|
idx = 0 |
||||||
|
for i in self.sounds: |
||||||
|
if i.name == name: |
||||||
|
yield idx |
||||||
|
if i.is_3d_stereo: |
||||||
|
yield idx + 1 |
||||||
|
break |
||||||
|
else: |
||||||
|
idx += 2 if i.is_3d_stereo else 1 |
||||||
|
else: |
||||||
|
raise ValueError(name) |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def register(cls): |
||||||
|
bpy.types.Sound.plasma_owned = BoolProperty(default=False, options={"HIDDEN"}) |
||||||
|
|
||||||
|
@property |
||||||
|
def requires_actor(self): |
||||||
|
return True |
||||||
|
|
||||||
|
|
||||||
|
@persistent |
||||||
|
def _toss_orphaned_sounds(scene): |
||||||
|
used_sounds = set() |
||||||
|
for i in bpy.data.objects: |
||||||
|
soundemit = i.plasma_modifiers.soundemit |
||||||
|
used_sounds.update((j.sound_data for j in soundemit.sounds)) |
||||||
|
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) |
@ -0,0 +1,105 @@ |
|||||||
|
# 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/>. |
||||||
|
|
||||||
|
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, "name", emboss=False, icon=icon, text="") |
||||||
|
layout.prop(item, "enabled", text="") |
||||||
|
else: |
||||||
|
layout.label("[Empty]") |
||||||
|
|
||||||
|
|
||||||
|
def soundemit(modifier, layout, context): |
||||||
|
row = layout.row() |
||||||
|
row.template_list("SoundListUI", "sounds", modifier, "sounds", modifier, "active_sound_index", |
||||||
|
rows=2, maxrows=3) |
||||||
|
col = row.column(align=True) |
||||||
|
op = col.operator("object.plasma_modifier_collection_add", icon="ZOOMIN", text="") |
||||||
|
op.modifier = modifier.pl_id |
||||||
|
op.collection = "sounds" |
||||||
|
op = col.operator("object.plasma_modifier_collection_remove", icon="ZOOMOUT", text="") |
||||||
|
op.modifier = modifier.pl_id |
||||||
|
op.collection = "sounds" |
||||||
|
op.index = modifier.active_sound_index |
||||||
|
|
||||||
|
try: |
||||||
|
sound = modifier.sounds[modifier.active_sound_index] |
||||||
|
except: |
||||||
|
pass |
||||||
|
else: |
||||||
|
# Sound datablock picker |
||||||
|
row = layout.row(align=True) |
||||||
|
row.prop_search(sound, "sound_data", bpy.data, "sounds", text="") |
||||||
|
open_op = row.operator("sound.plasma_open", icon="FILESEL", text="") |
||||||
|
open_op.data_path = repr(sound) |
||||||
|
open_op.sound_property = "sound_data" |
||||||
|
|
||||||
|
# Pack/Unpack |
||||||
|
data = bpy.data.sounds.get(sound.sound_data) |
||||||
|
if data is not None: |
||||||
|
if data.packed_file is None: |
||||||
|
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…
Reference in new issue