Serious-Engine/Sources/Engine/Sound/SoundObject.cpp

712 lines
20 KiB
C++
Raw Normal View History

2016-03-12 01:20:51 +01:00
/* Copyright (c) 2002-2012 Croteam Ltd.
This program is free software; you can redistribute it and/or modify
it under the terms of version 2 of the GNU General Public License as published by
the Free Software Foundation
This program 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 this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */
2016-03-11 14:57:17 +01:00
#include "Engine/StdH.h"
2016-03-11 14:57:17 +01:00
#include <Engine/Base/Stream.h>
#include <Engine/Base/Console.h>
#include <Engine/Base/CRC.h>
#include <Engine/Math/Vector.h>
#include <Engine/Math/Functions.h>
#include <Engine/Sound/SoundObject.h>
#include <Engine/Sound/SoundDecoder.h>
#include <Engine/Sound/SoundData.h>
#include <Engine/Sound/SoundLibrary.h>
#include <Engine/Sound/SoundListener.h>
#include <Engine/Entities/Entity.h>
#include <Engine/Entities/InternalClasses.h>
#include <Engine/Base/ListIterator.inl>
#include <Engine/Templates/Stock_CSoundData.h>
// sound event codes for prediction
#define EVENT_SOUNDPLAY 0x0101
#define EVENT_SOUNDSTOP 0x0102
#define EVENT_SOUNDSETOFFSET 0x0103
extern FLOAT snd_fEarsDistance;
extern FLOAT snd_fDelaySoundSpeed;
extern FLOAT snd_fDopplerSoundSpeed;
extern FLOAT snd_fPanStrength;
extern FLOAT snd_fLRFilter;
extern FLOAT snd_fBFilter;
extern FLOAT snd_fUFilter;
extern FLOAT snd_fDFilter;
extern BOOL _bPredictionActive;
// console variables for volume
extern FLOAT snd_fSoundVolume;
extern FLOAT snd_fMusicVolume;
static CTString GetPred(CEntity*pen)
{
CTString str1;
if (pen->IsPredictor()) {
str1 = "predictor";
} else if (pen->IsPredicted()) {
str1 = "predicted";
} else if (pen->en_ulFlags & ENF_WILLBEPREDICTED) {
str1 = "will be predicted";
} else {
str1 = "???";
}
CTString str;
str.PrintF("%08x-%s", pen, (const char *) str1);
2016-03-11 14:57:17 +01:00
return str;
}
/* ====================================================
*
* Class global methods
*/
/*
* Constructor
*/
CSoundObject::CSoundObject()
{
so_pCsdLink = NULL;
so_psdcDecoder = NULL;
so_penEntity = NULL;
so_slFlags = SOF_NONE;
2016-03-11 14:57:17 +01:00
// clear sound settings
so_spNew.sp_fLeftVolume = 1.0f;
so_spNew.sp_fRightVolume = 1.0f;
so_spNew.sp_slLeftFilter = 0x7FFF;
so_spNew.sp_slRightFilter = 0x7FFF;
so_spNew.sp_fPitchShift = 1.0f;
so_spNew.sp_fPhaseShift = 0.0f;
so_spNew.sp_fDelay = 0.0f;
so_sp = so_spNew;
so_fLeftOffset = 0.0f;
so_fRightOffset = 0.0f;
so_fOffsetDelta = 0.0f;
so_fDelayed = 0.0f;
so_fLastLeftVolume = 1.0f;
so_fLastRightVolume = 1.0f;
so_swLastLeftSample = 0;
so_swLastRightSample = 0;
// 3d effects
so_sp3.sp3_fFalloff = 0.0f;
so_sp3.sp3_fHotSpot = 0.0f;
so_sp3.sp3_fMaxVolume = 0.0f;
so_sp3.sp3_fPitch = 1.0f;
}
2016-03-11 14:57:17 +01:00
/*
* Destructor
*/
CSoundObject::~CSoundObject()
{
Stop_internal();
}
2016-03-11 14:57:17 +01:00
// copy from another object of same class
void CSoundObject::Copy(CSoundObject &soOther)
{
Stop_internal();
so_sp = so_spNew = soOther.so_sp;
so_sp3 = soOther.so_sp3;
so_penEntity = NULL;
so_slFlags = soOther.so_slFlags;
if (soOther.so_slFlags&SOF_PLAY) {
Play(soOther.so_pCsdLink, soOther.so_slFlags);
}
}
// Set 3D parameters
void CSoundObject::Set3DParameters( FLOAT fFalloff, FLOAT fHotSpot,
FLOAT fMaxVolume, FLOAT fPitch)
{
ASSERT( (fFalloff > 0) && (fHotSpot >= 0));
ASSERT( fMaxVolume >= 0);
ASSERT( fFalloff >= fHotSpot);
ASSERT( fPitch > 0);
CSoundObject *pso = this;
// if the sound's entity is a predictor
if (_bPredictionActive && so_penEntity!=NULL) {
if (so_penEntity->IsPredictionHead()) {
// get your prediction tail
//CPrintF("SET3D: ");
CEntity *pen = so_penEntity->GetPredictionTail();
if (pen!=so_penEntity) {
pso = (CSoundObject *)( ((UBYTE*)pen) + (size_t(this)-size_t(so_penEntity)) );
2016-03-11 14:57:17 +01:00
}
}
}
pso->so_sp3.sp3_fFalloff = fFalloff;
pso->so_sp3.sp3_fHotSpot = fHotSpot;
pso->so_sp3.sp3_fMaxVolume = fMaxVolume;
pso->so_sp3.sp3_fPitch = fPitch;
}
2016-03-11 14:57:17 +01:00
/* ====================================================
* Sound control methods
*/
// get proper sound object for predicted events - return NULL the event is already predicted
CSoundObject *CSoundObject::GetPredictionTail(ULONG ulTypeID, ULONG ulEventID)
{
// if the sound has an entity
if (so_penEntity!=NULL) {
//CPrintF(" {%s}", GetPred(so_penEntity));
// if the entity is temporary predictor
if (so_penEntity->GetFlags()&ENF_TEMPPREDICTOR) {
//CPrintF(" temppred\n");
// it must not play the sound
return NULL;
}
SLONG slOffset = size_t(this)-size_t(so_penEntity);
2016-03-11 14:57:17 +01:00
ULONG ulCRC;
CRC_Start(ulCRC);
CRC_AddLONG(ulCRC, slOffset);
CRC_AddLONG(ulCRC, ulTypeID);
CRC_Finish(ulCRC);
// if the event is predicted
if (so_penEntity->CheckEventPrediction(ulCRC, ulEventID)) {
//CPrintF(" predicted\n");
// return nothing
return NULL;
}
CEntity *pen = so_penEntity;
// find eventual prediction tail sound object
if (pen->IsPredictor()) {
pen = pen->GetPredictionTail();
if (pen!=so_penEntity) {
//CPrintF(" ROUTED\n");
return (CSoundObject *)( ((UBYTE*)pen) + slOffset );
}
}
}
// if no specific prediction states - use this object
//CPrintF(" ORIGINAL\n");
return this;
}
/*
* Play
*/
void CSoundObject::Play(CSoundData *pCsdLink, SLONG slFlags)
{
// synchronize access to sounds
CTSingleLock slSounds( &_pSound->sl_csSound, TRUE);
//CPrintF("PLAY: '%s'", (const char*)pCsdLink->GetName().FileName());
// get prediction tail
STUBBED("64-bit issue");
CSoundObject *psoTail = GetPredictionTail(EVENT_SOUNDPLAY, (ULONG)(size_t)pCsdLink);
2016-03-11 14:57:17 +01:00
// if the event is predicted
if (psoTail==NULL) {
// do nothing;
return;
}
// play the sound in the given object
psoTail->Play_internal(pCsdLink, slFlags);
}
// play sound - internal function - doesn't account for prediction
void CSoundObject::Play_internal( CSoundData *pCsdLink, SLONG slFlags)
{
ASSERT(so_penEntity==NULL || !so_penEntity->IsPredictor());
// check if should continue with new sound
BOOL bContinue =
((slFlags&SOF_SMOOTHCHANGE) &&
(so_slFlags&SOF_PREPARE) &&
(so_slFlags&SOF_PLAY));
Stop_internal();
// mark new data as referenced once more
pCsdLink->AddReference();
// mark old data as referenced once less
so_pCsdLink->RemReference();
// store init SoundData
so_pCsdLink = pCsdLink;
// add to link list
so_pCsdLink->AddObjectLink(*this);
// store flags
so_slFlags = slFlags;
// if should continue with new sound
if (bContinue) {
// play buffer immediately
so_slFlags = so_slFlags | SOF_PREPARE | SOF_PLAY;
} else {
// play buffer
so_slFlags = (so_slFlags & ~(SOF_PREPARE|SOF_PAUSED)) | SOF_PLAY;
}
// if the sound data is encoded
if (so_pCsdLink->sd_ulFlags&SDF_ENCODED) {
// create decoder
if (so_pCsdLink->sd_ulFlags&SDF_STREAMING) {
so_psdcDecoder = new CSoundDecoder(so_pCsdLink->GetName());
} else {
ASSERT(FALSE); // nonstreaming not supported anymore
}
}
// remember starting parameters
so_sp = so_spNew;
// initialize mixer temporary variables
if (!(slFlags&SOF_LOADED)) {
so_fLastLeftVolume = so_sp.sp_fLeftVolume;
so_fLastRightVolume = so_sp.sp_fRightVolume;
so_fLeftOffset = 0.0f;
so_fRightOffset = 0.0f;
so_fOffsetDelta = 0.0f;
so_fDelayed = 0.0f;
if (!bContinue) {
so_swLastLeftSample = 0;
so_swLastRightSample = 0;
} else {
// adjust for master volume
if(so_slFlags&SOF_MUSIC) {
so_fLastLeftVolume *= snd_fMusicVolume;
so_fLastRightVolume *= snd_fMusicVolume;
} else {
so_fLastLeftVolume *= snd_fSoundVolume;
so_fLastRightVolume *= snd_fSoundVolume;
}
}
}
}
// hard set sound offset in seconds
void CSoundObject::SetOffset( FLOAT fOffset)
{
// synchronize access to sounds
CTSingleLock slSounds( &_pSound->sl_csSound, TRUE);
// get prediction tail
//CPrintF("SETOFF: ");
CSoundObject *psoTail = GetPredictionTail(EVENT_SOUNDSETOFFSET, 0);
// if the event is predicted
if (psoTail==NULL) {
// do nothing;
return;
}
// if sound not playing
if (psoTail->so_pCsdLink==NULL) {
// do nothing
return;
}
// safety check
ASSERT( fOffset>=0);
if( fOffset<0) {
CPrintF( "BUG: Trying to set negative offset (%.2g) in sound '%s' !\n", fOffset, (const char *) (CTString&)psoTail->so_pCsdLink->GetName());
2016-03-11 14:57:17 +01:00
fOffset = 0.0f;
}
// on the other hand, don't set offset for real - might be source for some bugs!
return;
// update sound offsets
CPrintF("Setting offset: %g\n", fOffset);
psoTail->so_fLeftOffset = psoTail->so_fRightOffset = psoTail->so_pCsdLink->sd_wfeFormat.nSamplesPerSec*fOffset;
}
/*
* Stop
*/
void CSoundObject::Stop(void)
{
// synchronize access to sounds
CTSingleLock slSounds( &_pSound->sl_csSound, TRUE);
//CPrintF("STOP");
if (so_pCsdLink!=NULL) {
//CPrintF(" '%s'", (const char*)so_pCsdLink->GetName().FileName());
}
CSoundObject *psoTail = this;
// get prediction tail
STUBBED("64-bit issue");
psoTail = GetPredictionTail(EVENT_SOUNDSTOP, (ULONG)(size_t)so_pCsdLink);
2016-03-11 14:57:17 +01:00
// if the event is predicted
if (psoTail==NULL) {
// do nothing;
return;
}
psoTail->Stop_internal();
}
void CSoundObject::Stop_internal(void)
{
// sound is stopped
2016-03-11 14:57:17 +01:00
so_slFlags &= ~(SOF_PLAY|SOF_PREPARE|SOF_PAUSED);
// destroy decoder if exists
if( so_psdcDecoder!=NULL) {
delete so_psdcDecoder;
so_psdcDecoder = NULL;
}
// if added in link list, remove it from list
if( IsHooked()) {
ASSERT(so_pCsdLink != NULL);
so_pCsdLink->RemoveObjectLink(*this);
// remove reference from SoundData
so_pCsdLink->RemReference();
// clear SoundData link
so_pCsdLink = NULL;
}
}
2016-03-11 14:57:17 +01:00
// Update all 3d effects
void CSoundObject::Update3DEffects(void)
{
// if not 3d sound
if( !(so_slFlags & SOF_3D)) {
// do nothing;
return;
}
// if (!(so_slFlags&SOF_PREPARE)) {
// if the sound's entity is a predictor
/* if (so_penEntity!=NULL && so_penEntity->IsPredictor()) {
// kill the sound
so_slFlags&=~SOF_PLAY;
//CPrintF("Update canceled %s (%s)\n", (const char*)so_pCsdLink->GetName(), GetPred(so_penEntity));
// do nothing;
return;
}
*/
//CPrintF("Update PASSED %s (%s)\n", (const char*)so_pCsdLink->GetName(), GetPred(so_penEntity));
// }
// total parameters (accounting for all listeners)
FLOAT fTLVolume = 0, fTRVolume = 0;
FLOAT fTLFilter = UpperLimit(0.0f), fTRFilter = UpperLimit(0.0f);
FLOAT fTLDelay = UpperLimit(0.0f), fTRDelay = UpperLimit(0.0f);
FLOAT fTPitchShift = 0;
// get your position parameters
FLOAT3D vPosition(0,0,0);
FLOAT3D vSpeed(0,0,0);
if (so_penEntity!=NULL) {
vPosition = so_penEntity->en_plPlacement.pl_PositionVector;
if (so_penEntity->en_ulPhysicsFlags&EPF_MOVABLE) {
CMovableEntity *penMovable = (CMovableEntity *)so_penEntity;
vSpeed = penMovable->en_vCurrentTranslationAbsolute;
}
}
// for each listener
INDEX ctEffectiveListeners = 0;
{FOREACHINLIST( CSoundListener, sli_lnInActiveListeners, _pSound->sl_lhActiveListeners, itsli)
{
CSoundListener &sli = *itsli;
// if local, but not of this listener
if ((so_slFlags&SOF_LOCAL) && so_penEntity!=sli.sli_penEntity) {
// don't add this listener
continue;
}
// calculated parameters for this listener
FLOAT fLVolume, fRVolume;
FLOAT fLFilter, fRFilter;
FLOAT fLDelay , fRDelay ;
FLOAT fPitchShift;
// calculate distance from listener
FLOAT3D vAbsDelta = vPosition - sli.sli_vPosition;
FLOAT fAbsDelta = vAbsDelta.Length();
// if too far away
if (fAbsDelta>so_sp3.sp3_fFalloff) {
// don't add this listener
continue;
}
// calculate distance falloff factor
FLOAT fDistanceFactor;
if( fAbsDelta <= so_sp3.sp3_fHotSpot) {
fDistanceFactor = 1;
} else {
fDistanceFactor = (so_sp3.sp3_fFalloff - fAbsDelta) /
(so_sp3.sp3_fFalloff - so_sp3.sp3_fHotSpot);
}
ASSERT(fDistanceFactor>=0 && fDistanceFactor<=+1);
// calculate volumetric influence
// NOTE: decoded sounds must be treated as volumetric
2016-03-11 14:57:17 +01:00
FLOAT fNonVolumetric = 1.0f;
FLOAT fNonVolumetricAdvanced = 1.0f;
if( (so_slFlags & SOF_VOLUMETRIC) || so_psdcDecoder!=NULL) {
fNonVolumetric = 1.0f-fDistanceFactor;
fNonVolumetricAdvanced = 0.0f;
}
ASSERT(fNonVolumetric>=0 && fNonVolumetric<=+1);
// find doppler effect pitch shift
fPitchShift = 1.0f;
if (fAbsDelta>0.001f) {
FLOAT3D vObjectDirection = vAbsDelta/fAbsDelta;
FLOAT fObjectSpeed = vSpeed%vObjectDirection; // negative towards listener
FLOAT fListenerSpeed = sli.sli_vSpeed%vObjectDirection; // positive towards object
fPitchShift =
(snd_fDopplerSoundSpeed+fListenerSpeed*fNonVolumetricAdvanced)/
(snd_fDopplerSoundSpeed+fObjectSpeed*fNonVolumetricAdvanced);
}
// find position of sound relative to viewer orientation
FLOAT3D vRelative = vAbsDelta*!sli.sli_mRotation;
// find distances from left and right ear
FLOAT fLDistance = (FLOAT3D(-snd_fEarsDistance*fNonVolumetricAdvanced/2,0,0)-vRelative).Length();
FLOAT fRDistance = (FLOAT3D(+snd_fEarsDistance*fNonVolumetricAdvanced/2,0,0)-vRelative).Length();
// calculate sound delay to each ear
fLDelay = fLDistance/snd_fDelaySoundSpeed;
fRDelay = fRDistance/snd_fDelaySoundSpeed;
// calculate relative sound directions
FLOAT fLRFactor=0; // positive right
FLOAT fFBFactor=0; // positive front
FLOAT fUDFactor=0; // positive up
if (fAbsDelta>0.001f) {
FLOAT3D vDir = vRelative/fAbsDelta;
fLRFactor = +vDir(1);
fFBFactor = -vDir(3);
fUDFactor = +vDir(2);
}
ASSERT(fLRFactor>=-1.1 && fLRFactor<=+1.1);
ASSERT(fFBFactor>=-1.1 && fFBFactor<=+1.1);
ASSERT(fUDFactor>=-1.1 && fUDFactor<=+1.1);
// calculate panning influence factor
FLOAT fPanningFactor= fNonVolumetric*snd_fPanStrength;
ASSERT(fPanningFactor>=0 && fPanningFactor<=+1);
// calc volume for left and right channel
FLOAT fVolume = so_sp3.sp3_fMaxVolume * fDistanceFactor;
if( fLRFactor > 0) {
fLVolume = (1-fLRFactor*fPanningFactor) * fVolume;
fRVolume = fVolume;
} else {
fLVolume = fVolume;
fRVolume = (1+fLRFactor*fPanningFactor) * fVolume;
}
// calculate filters
FLOAT fListenerFilter = sli.sli_fFilter;
if (so_slFlags&SOF_NOFILTER) {
fListenerFilter = 0.0f;
}
fLFilter = fRFilter = 1+fListenerFilter;
if( fLRFactor > 0) {
fLFilter += fLRFactor*snd_fLRFilter*fNonVolumetricAdvanced;
} else {
fRFilter -= fLRFactor*snd_fLRFilter*fNonVolumetricAdvanced;
}
if( fFBFactor<0) {
fLFilter -= snd_fBFilter*fFBFactor*fNonVolumetricAdvanced;
fRFilter -= snd_fBFilter*fFBFactor*fNonVolumetricAdvanced;
}
if( fUDFactor>0) {
fLFilter += snd_fUFilter*fUDFactor*fNonVolumetricAdvanced;
fRFilter += snd_fUFilter*fUDFactor*fNonVolumetricAdvanced;
} else {
fLFilter -= snd_fDFilter*fUDFactor*fNonVolumetricAdvanced;
fRFilter -= snd_fDFilter*fUDFactor*fNonVolumetricAdvanced;
}
// adjust calculated volume to the one of listener
fLVolume *= sli.sli_fVolume;
fRVolume *= sli.sli_fVolume;
// update parameters for all listener
fTLVolume = Max( fTLVolume, fLVolume);
fTRVolume = Max( fTRVolume, fRVolume);
fTLDelay = Min( fTLDelay , fLDelay );
fTRDelay = Min( fTRDelay , fRDelay );
fTLFilter = Min( fTLFilter, fLFilter);
fTRFilter = Min( fTRFilter, fRFilter);
fTPitchShift += fPitchShift;
ctEffectiveListeners++;
}}
fTPitchShift /= ctEffectiveListeners;
// calculate 2d parameters
FLOAT fPitchShift = fTPitchShift * so_sp3.sp3_fPitch;
FLOAT fPhaseShift = fTLDelay-fTRDelay;
FLOAT fDelay = Min( fTRDelay,fTLDelay);
// CPrintF("V:%f %f F:%f %f P:%f S:%f\n",
// fTLVolume, fTRVolume,
// fTLFilter, fTRFilter,
// fPhaseShift,
// fPitchShift);
// set sound parameters
fTLVolume = Clamp( fTLVolume, SL_VOLUME_MIN, SL_VOLUME_MAX);
fTRVolume = Clamp( fTRVolume, SL_VOLUME_MIN, SL_VOLUME_MAX);
SetVolume( fTLVolume, fTRVolume);
if( fTLVolume>0 || fTRVolume>0) {
// do safety clamping
fTLFilter = ClampDn( fTLFilter, 1.0f);
fTRFilter = ClampDn( fTRFilter, 1.0f);
fDelay = ClampDn( fDelay, 0.0f);
fPitchShift = ClampDn( fPitchShift, 0.001f);
fPhaseShift = Clamp( fPhaseShift, -1.0f, +1.0f);
// set sound params
SetFilter( fTLFilter, fTRFilter);
SetDelay( fDelay);
SetPitch( fPitchShift);
SetPhase( fPhaseShift);
}
}
// Prepare sound
void CSoundObject::PrepareSound(void)
{
ASSERT(so_penEntity==NULL || !so_penEntity->IsPredictor());
so_fLastLeftVolume = so_spNew.sp_fLeftVolume;
so_fLastRightVolume = so_spNew.sp_fRightVolume;
// adjust for master volume
if(so_slFlags&SOF_MUSIC) {
so_fLastLeftVolume *= snd_fMusicVolume;
so_fLastRightVolume *= snd_fMusicVolume;
} else {
so_fLastLeftVolume *= snd_fSoundVolume;
so_fLastRightVolume *= snd_fSoundVolume;
}
}
2016-03-11 14:57:17 +01:00
// Obtain sound and play it for this object
void CSoundObject::Play_t(const CTFileName &fnmSound, SLONG slFlags) // throw char *
{
// obtain it (adds one reference)
CSoundData *ptd = _pSoundStock->Obtain_t(fnmSound);
// set it as data (adds one more reference, and remove old reference)
Play(ptd, slFlags);
// release it (removes one reference)
_pSoundStock->Release(ptd);
// total reference count +1+1-1 = +1 for new data -1 for old data
}
2016-03-11 14:57:17 +01:00
// read/write functions
void CSoundObject::Read_t(CTStream *pistr) // throw char *
{
int iDroppedOut;
// load file name
CTFileName fnmSound;
*pistr >> fnmSound;
// load object preferences
*pistr >> iDroppedOut;
*pistr >> so_slFlags;
*pistr >> so_spNew.sp_fLeftVolume;
*pistr >> so_spNew.sp_fRightVolume;
*pistr >> so_spNew.sp_slLeftFilter;
*pistr >> so_spNew.sp_slRightFilter;
*pistr >> so_spNew.sp_fPitchShift;
*pistr >> so_spNew.sp_fPhaseShift;
*pistr >> so_spNew.sp_fDelay;
*pistr >> so_fDelayed;
*pistr >> so_fLastLeftVolume;
*pistr >> so_fLastRightVolume;
*pistr >> so_swLastLeftSample;
*pistr >> so_swLastRightSample;
*pistr >> so_fLeftOffset;
*pistr >> so_fRightOffset;
*pistr >> so_fOffsetDelta;
// load 3D parameters
so_penEntity = NULL;
*pistr >> so_sp3.sp3_fFalloff;
*pistr >> so_sp3.sp3_fHotSpot;
*pistr >> so_sp3.sp3_fMaxVolume;
*pistr >> so_sp3.sp3_fPitch;
// update current state
so_sp = so_spNew;
// Obtain and play object (sound)
if ( fnmSound != "" && (so_slFlags&SOF_PLAY)) {
Play_t( fnmSound, so_slFlags|SOF_LOADED);
}
}
2016-03-11 14:57:17 +01:00
void CSoundObject::Write_t(CTStream *pistr) // throw char *
{
int iDroppedOut=0;
// save file name
if (so_pCsdLink!=NULL) {
*pistr << (so_pCsdLink->GetName());
} else {
*pistr << CTFILENAME("");
}
// save object preferences
*pistr << iDroppedOut;
*pistr << so_slFlags;
*pistr << so_spNew.sp_fLeftVolume;
*pistr << so_spNew.sp_fRightVolume;
*pistr << so_spNew.sp_slLeftFilter;
*pistr << so_spNew.sp_slRightFilter;
*pistr << so_spNew.sp_fPitchShift;
*pistr << so_spNew.sp_fPhaseShift;
*pistr << so_spNew.sp_fDelay;
*pistr << so_fDelayed;
*pistr << so_fLastLeftVolume;
*pistr << so_fLastRightVolume;
*pistr << so_swLastLeftSample;
*pistr << so_swLastRightSample;
*pistr << so_fLeftOffset;
*pistr << so_fRightOffset;
*pistr << so_fOffsetDelta;
// save 3D parameters
*pistr << so_sp3.sp3_fFalloff;
*pistr << so_sp3.sp3_fHotSpot;
*pistr << so_sp3.sp3_fMaxVolume;
*pistr << so_sp3.sp3_fPitch;
}