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