-- 3D SOUND MANAGER -- -- Parent Script -- ---------------------------------------------------------------------- -- 060523 JN: Added plReserve and the ability to play sounds once, -- then move them to the plReserve list -- 060423 JN: version 0.1 ---------------------------------------------------------------------- -- An instance of this script can be used to manage the spatial -- arrangement of sounds with respect to a 3D player character. If no -- 3D character is defined, then a position of vector(0, 0, 0) is -- assumed. -- -- The character is assumed to be facing down the negative z-axis, -- with its ears either side of the centre of the given transform. -- -- Sounds are registered as having a particular location in 3D space -- and belonging (optionally) to a particular zone. The location is -- defined by a transform, which may belong to a moving 3D object. -- The pan and volume of the sound will depend on the distance of the -- character from the sound, and the character's rotation about the -- y-axis. -- -- If no zones are defined for a given sound, it is assumed that it -- can be heard in any zone, so long as it is within earshot. If the -- 3D character is not currently in any of the zones defined for a -- sound, then that sound will not be heard. Zone sounds have -- priority over non-zone sounds. -- -- You can set a new zone using the Sound_SetZone() call. This takes -- two parameters: the zone name and an integer indicating the number -- of milliseconds that the sound should take to fade out. -- -- A given sound source may produce a variety of sounds, so each -- sound source is defined by: -- -- * A symbol name (e.g. #NPC_1) -- * A property list of sound members (e.g. [#song: member "Song"]) -- * A transform. This may be the transform of a moving object. -- * A radius. Sounds are only heard if the player is inside this -- radius. They are loudest at the centre and diminish as the -- square of the distance from the centre. If a radius of 0 is -- used, the sound does not diminish. -- -- You can add any number of sound sources, but the more you add, the -- longer it takes this Manager to decide which ones should currently -- be playing. This calculation takes place on every enterFrame. -- It is thus best to keep the number of concurrent sounds down. -- -- This instance checks on every enterFrame which sound sources are -- currently audible, and allocates channels to those sounds which -- have the highest volume or priority. -- Sounds which are not currently audible can "continue" silently: -- when they become audible again, they will carry on from where they -- would have got to had they been audible all the time. This is -- useful for music or the spoken word. Alternatively, sounds can -- be set to restart when they are resumed. This is useful for -- noise or repetitive sounds. -- -- -- DEPENDENCIES -- ------------ -- Requires the following script: -- * Event Broker Movie Script -- to register for #enterFrame event -- * Sound Source Parent Script --sound close --sound fadeIn --breakLoop --fadeIn --fadeOut --fadeTo --sound fadeOut --sound playFile --sound stop --pause --queue --stop -- ---------------------------------------------------------------------- -- PROPERTY DECLARATIONS -- property pSourceScript -- script("Sound Source") property plSources -- [#sourceName: , ...] -- (active sounds) property plReserve -- [#sourceName: , ...] -- (sounds not currently active) property plChannels -- [: #sourceName, ...] property pChannelCount -- number of channels managed by this instance property pTransform -- pointer to transform of 3D character property pZone -- symbol name of current zone -- CONSTRUCTOR METHOD -- on new(me, aStartChannel, anEndChannel) ------------------------------ -- INPUT: and should be an integer -- between 1 and 8. If no valid values are given, 1 and 8 -- are assumed respectively. -------------------------------------------------------------------- pSourceScript = script("Sound Source") -- HARD-CODED script name if not pSourceScript then return xCeption(#sound, "Sound Source script missing") end if plSources = [:] plReserve = [:] -- Channels vMaxChannel = 8 if not integerP(aStartChannel) then aStartChannel = 1 else aStartChannel = max(1, min(aStartChannel, vMaxChannel)) end if if not integerP(anEndChannel) then anEndChannel = 8 else anEndChannel = max(aStartChannel, min(anEndChannel, vMaxChannel)) end if -- Set all ascribed channels to 0 plChannels = [:] repeat with i = aStartChannel to anEndChannel plChannels.addProp(i, 0) end repeat pChannelCount = anEndChannel - aStartChannel + 1 -- Assume that there will be no moving character, and that it is -- not currently in any zone pTransform = transform() pZone = 0 -- Update sound positions on enterFrame Event_Register(#enterFrame, me) return me end new on Finalize(me) ------------------------------------------------------ -- ACTION: Ensures that there are no circular references -------------------------------------------------------------------- call(#Finalize, plSources) plSources.deleteAll() pTransform = VOID end Finalize on Sound_SetPlayerTransform(me, aTransform) -------------------------- -- INPUT: may be a point, a vector, a transform or a -- a 3D object. If it is a transform or an object, the -- position of the player will change as the transform (of -- the object) changes. If no valid position data is given -- pTransform is reset to the centre of the 3D world: -- vector(0, 0, 0). -- ACTION: (Adapts non-transform input to a transform, then) Adopts -- the given transform as the current position of the player -- OUTPUT: Returns 0 for no error. -------------------------------------------------------------------- case ilk(aTransform) of #transform, #model, #camera, #group: --, #light -- Continue. Transform may be dynamic. #point: -- Static vVector = vector(aTransform.locH,0,aTransform.locV) aTransform = transform() aTransform.position = vVector #vector: -- Static vVector = aTransform aTransform = transform() aTransform.position = vVector otherwise: -- Static at the centre of the world aTransform = transform() end case pTransform = aTransform return 0 end Sound_SetPlayerTransform on Sound_RegisterSource(me, aPropList) ------------------------------- -- INPUT: should be a property list with the format: -- [#symbol: {, -- #soundMembers: [: , ...]}{, -- #transform: <3D transform>}{, -- #radius: }{, -- #zones: }{, -- #maxVolume: }{, -- #play: <#unique| #once | #loop | #multiple >}{, -- #priority: }{, -- -- #channel: }] -- Alternatively, aPropList could simply be a symbol name, -- in which case default values for all the other properties -- will be used. -- ACTION: Creates a new Sound Source instance and stores it in the -- plRegistly list. If -- OUTPUT: -------------------------------------------------------------------- case ilk(aPropList) of #propList: vSymbol = aPropList[#symbol] if not symbolP(vSymbol) then return #symbolExpected end if #symbol: vSymbol = aPropList aPropList = [#symbol: vSymbol] otherwise: return #propListExpected end case vInstance = pSourceScript.new(aPropList) case aPropList.play of #unique, #once: -- Wait for an explicit #Source_Play event plReserve[vSymbol] = vInstance otherwise: -- This sound is continuous or may be repeated plSources[vSymbol] = vInstance end case end Sound_RegisterSource on Sound_SetSourceTransform(me, aSourceSymbol, aTransform) ----------- -- -------------------------------------------------------------------- if not symbolP(aSourceSymbol) then return #symbolExpected end if vSource = plSources[aSourceSymbol] if ilk(vSource) <> #instance then return #unknownSource end if return vSource.Source_SetTransform(aTransform) end Sound_SetSourceTransform on Sound_Play(me, aSourceSymbol, aPropList) -------------------------- -- INPUT: should be a symbol -- may be a property list with one or more of the -- properties below: -- [#soundMembers: [: , ...], -- #transform: <3D transform>, -- #radius: , -- #zones: , -- #maxVolume: , -- #play: <#unique| #once | #loop | #multiple >, -- #priority: ] -- ACTION: Allows the given sound to start playing on the next -- #enterFrame, if its volume and priority are high enough. -------------------------------------------------------------------- if not symbolP(aSourceSymbol) then return #symbolExpected end if vSource = plSources[aSourceSymbol] if ilk(vSource) <> #instance then vSource = plReserve[aSourceSymbol] if ilk(vSource, #instance) then -- Move this source back onto the plSources list plReserve.deleteProp(aSourceSymbol) plSources.addProp(aSourceSymbol, vSource) if not (vSource.plSoundMembers).count() then xCeption(#sound, "No sound member for source: "&aSourceSymbol) end if else return #unknownSource end if end if --return vSource.Source_Start(aPropList) return 0 end Sound_Play on Sound_GetState(me, aSourceSymbol) --------------------------------- -- INPUT: should be a symbol -- OUTPUT: -------------------------------------------------------------------- vState = 0 if symbolP(aSourceSymbol) then vSource = plSources[aSourceSymbol] if ilk(vSource, #instance) then -- The sound exists and may currently be playing vState = vSource.Source_GetState() end if end if return vState end Sound_GetState on Sound_Delete(me, aSourceSymbol) ----------------------------------- -- -------------------------------------------------------------------- plSources.deleteProp(aSourceSymbol) end Sound_Delete on Sound_Reserve(me, aSourceSymbol, anInstance) ---------------------- -- -------------------------------------------------------------------- plSources.deleteProp(aSourceSymbol) if not plReserve.findPos(aSourceSymbol) then plReserve.appProp(aSourceSymbol, anInstance) end if end Sound_Reserve --==================================================================-- on __EVENT_HANDLER__ end --==============================================================-- on enterFrame(me) ---------------------------------------------------- -- SOURCE: Forwarded by the Event Broker -- ACTION: Checks the current position and orientation of the 3D -- character and sets the volume and pan of the various -- registered sounds accordingly -------------------------------------------------------------------- -- vTrace = the trace -- if vTrace then -- -- Don't trace all the sound management data -- the trace = FALSE -- put "Updating sounds" -- end if me.mSound_UpdateSources() -- if vTrace then -- the trace = TRUE -- end if end enterFrame --==================================================================-- on __PRIVATE_METHODS__ end --==============================================================-- on mSound_UpdateSources(me) ------------------------------------------ -- SOURCE: Sent by enterFrame() -- ACTION: Checks the current position and orientation of the 3D -- character and sets the volume and pan of the various -- registered sounds accordingly -------------------------------------------------------------------- vSourceList = [:] -- [: #sourceName, ...] vSourceList.sort() -- loudest volumes at end of list if ilk(pTransform, #transform) then vTransform = pTransform else -- Use the current transform of this moving object vTransform = pTransform.getWorldTransform() end if call(#Source_GetStatus, plSources, vTransform, pZone, vSourceList) -- This call updates the volume and pan data for all sources -- Delete any sounds to quiet to be played vCount = vSourceList.count i = vCount - pChannelCount if i > 0 then repeat while i -- TODO: retain any quieter sounds that have a high priority vSourceList.deleteAt(i) i = i - 1 end repeat end if if not max(plChannels) then -- There are currently no sounds registered else -- vSourceList now only contains sources that we want to hear. We -- now need to suspend any currently playing sources that are not -- in vSourceList. me.mSound_SuspendLowVolume(vSourceList) end if i = vSourceList.count repeat while i vSourceName = vSourceList[i] vSourceInstance = plSources[vSourceName] vChannel = plChannels.getOne(vSourceName) if not vChannel then -- No channel is currently ascribed to this sound. Grab a new -- channel (suspend a quieter sound if necessary). vChannel = me.mSound_ClaimFreeChannel(vSourceName) vSourceInstance.Source_SetChannel(vChannel) vSourceInstance.Source_Resume() end if i = i - 1 end repeat call(#Source_Update, plSources) end mSound_UpdateSources on mSound_SuspendLowVolume(me, aSourceList) -------------------------- -- SOURCE: Sent by mSound_UpdateSources() -- INPUT: is a property list with the format: -- [: , ...] -- ACTION: Looks for any sources in plChannels which are not in -- aSourceList, suspends them and frees the channel. -------------------------------------------------------------------- i = plChannels.count repeat while i vSourceName = plChannels[i] if not vSourceName then -- This channel is free else if not aSourceList.getPos(vSourceName) then -- This sound is too quiet to be heard. Suspend it. vSourceInstance = plSources[vSourceName] vSourceInstance.Source_Suspend(me) -- 'me' is for callback -- Indicate that the channel is now free plChannels[i] = 0 end if i = i - 1 end repeat end mSound_SuspendLowVolume on mSound_ClaimFreeChannel(me, aSourceName) ------------------------- -- SOURCE: Sent by mSound_UpdateSources() -- INPUT: will be the symbol name of a source that -- currently does not have a channel -- ACTION: Finds a free channel and claims it for aSourceName -- OUTPUT: Returns the integer number of the claimed channel -- (or an error symbol) -------------------------------------------------------------------- vIndex = plChannels.getPos(0) if vIndex then -- This channel is not currently being used vChannel = plChannels.getPropAt(vIndex) plChannels[vIndex] = aSourceName return vChannel end if return xCeption(#sound, "No channels are free for #"&aSourceName) end mSound_GetFreeChannel