152 | function SoundManager:cloneSample(sample, linkNode, modifierTargetObject) |
153 | local newSample = table.copy(sample) |
154 | newSample.modifiers = table.copy(sample.modifiers) |
155 | |
156 | if not sample.is2D then |
157 | newSample.soundNode = createAudioSource(newSample.sampleName, newSample.filename, newSample.outerRadius, newSample.innerRadius, newSample.current.volume, newSample.loops) |
158 | newSample.soundSample = getAudioSourceSample(newSample.soundNode) |
159 | setAudioSourceAutoPlay(newSample.soundNode, false) |
160 | link(linkNode, newSample.soundNode) |
161 | newSample.linkNode = linkNode |
162 | |
163 | if newSample.linkNodeOffset == nil then |
164 | setTranslation(newSample.soundNode, 0, 0, 0) |
165 | else |
166 | setTranslation(newSample.soundNode, newSample.linkNodeOffset[1], newSample.linkNodeOffset[2], newSample.linkNodeOffset[3]) |
167 | end |
168 | end |
169 | |
170 | setSampleGroup(newSample.soundSample, sample.audioGroup) |
171 | newSample.audioGroup = sample.audioGroup |
172 | |
173 | if sample.supportsReverb then |
174 | addSampleEffect(newSample.soundSample, SoundManager.DEFAULT_REVERB_EFFECT) |
175 | else |
176 | removeSampleEffect(sample.soundSample, SoundManager.DEFAULT_REVERB_EFFECT) |
177 | end |
178 | |
179 | if modifierTargetObject ~= nil then |
180 | newSample.modifierTargetObject = modifierTargetObject |
181 | end |
182 | |
183 | newSample.sourceRandomizations = {} |
184 | for i=1, #sample.sourceRandomizations do |
185 | local randomSample = sample.sourceRandomizations[i] |
186 | |
187 | local newRandomSample = self:getRandomSample(sample, randomSample.filename) |
188 | |
189 | table.insert(newSample.sourceRandomizations, newRandomSample) |
190 | end |
191 | |
192 | self.samples[newSample] = newSample |
193 | table.insert(self.orderedSamples, newSample) |
194 | |
195 | return newSample |
196 | end |
203 | function SoundManager:cloneSample2D(sample, linkNode, modifierTargetObject) |
204 | local newSample = table.copy(sample) |
205 | newSample.modifiers = table.copy(sample.modifiers) |
206 | |
207 | newSample.audioGroup = sample.audioGroup |
208 | newSample.linkNode = nil |
209 | newSample.soundNode = nil |
210 | newSample.is2D = true |
211 | |
212 | newSample.soundSample = createSample(newSample.sampleName) |
213 | newSample.orgSoundSample = newSample.soundSample |
214 | |
215 | loadSample(newSample.soundSample, newSample.filename, false) |
216 | newSample.duration = getSampleDuration(newSample.soundSample) |
217 | setSampleGroup(newSample.soundSample, sample.audioGroup) |
218 | newSample.audioGroup = sample.audioGroup |
219 | |
220 | if modifierTargetObject ~= nil then |
221 | newSample.modifierTargetObject = modifierTargetObject |
222 | end |
223 | |
224 | newSample.sourceRandomizations = {} |
225 | for i=1, #sample.sourceRandomizations do |
226 | local randomSample = sample.sourceRandomizations[i] |
227 | |
228 | local newRandomSample = {} |
229 | newRandomSample.filename = randomSample.filename |
230 | newRandomSample.isEmpty = randomSample.isEmpty |
231 | newRandomSample.is2D = true |
232 | if not randomSample.isEmpty then |
233 | newRandomSample.soundSample = createSample(newSample.sampleName) |
234 | loadSample(newRandomSample.soundSample, newRandomSample.filename, false) |
235 | end |
236 | |
237 | table.insert(newSample.sourceRandomizations, newRandomSample) |
238 | end |
239 | |
240 | self.samples[newSample] = newSample |
241 | table.insert(self.orderedSamples, newSample) |
242 | |
243 | return newSample |
244 | end |
851 | function SoundManager:draw() |
852 | -- draw sample debug |
853 | for linkNode, linkNodeSamples in pairs(self.debugSamplesLinkNodes) do |
854 | -- draw link node |
855 | local x,y,z = getWorldTranslation(linkNode) |
856 | local debugNode = createTransformGroup("sampleDebugNode") |
857 | setTranslation(debugNode, x,y,z) |
858 | |
859 | local linkNodeText = string.format("LinkNode '%s' (visible=%s)", getName(linkNode), getEffectiveVisibility(linkNode)) |
860 | DebugUtil.drawDebugNode(linkNode, linkNodeText, false) |
861 | |
862 | -- draw invidiual samples |
863 | for i=1, #linkNodeSamples do |
864 | local sample = linkNodeSamples[i] |
865 | local name = sample.sampleName or i |
866 | local rotOffset = i/100 -- slightly offset rotation per sample so circles with same radius distinguishable |
867 | |
868 | local text = string.format("AudioSample '%s' IR=%d OR=%d isPlaying=%s tmpl=%s", name, sample.innerRadius, sample.outerRadius, self:getIsSamplePlaying(sample), sample.templateName) |
869 | local color = DebugUtil.tableToColor(sample) |
870 | setRotation(debugNode, 0, rotOffset, 0) |
871 | Utils.renderTextAtWorldPosition(x,y,z, text, getCorrectTextSize(0.016), i * getCorrectTextSize(0.016), color) |
872 | DebugUtil.drawDebugCircleAtNode(debugNode, sample.innerRadius, 20, color, true) |
873 | DebugUtil.drawDebugCircleAtNode(debugNode, sample.outerRadius, 20, color, true) |
874 | |
875 | -- redraw circle rotated 90deg |
876 | setRotation(debugNode, 0, math.rad(90)+rotOffset, 0) |
877 | DebugUtil.drawDebugCircleAtNode(debugNode, sample.innerRadius, 20, color, true) |
878 | DebugUtil.drawDebugCircleAtNode(debugNode, sample.outerRadius, 20, color, true) |
879 | end |
880 | delete(debugNode) |
881 | end |
882 | end |
656 | function SoundManager:loadModifiersFromXML(sample, xmlFile, key) |
657 | sample.modifiers = Utils.getNoNil(sample.modifiers, {}) |
658 | for _, attribute in pairs(SoundManager.SAMPLE_MODIFIER_ATTRIBUTES) do |
659 | local modifier = Utils.getNoNil(sample.modifiers[attribute], {}) |
660 | modifier.hasModification = Utils.getNoNil(modifier.hasModification, false) |
661 | |
662 | local i = 0 |
663 | while true do |
664 | local modKey = string.format("%s.%s.modifier(%d)", key, attribute, i) |
665 | if not hasXMLProperty(xmlFile, modKey) then |
666 | break |
667 | end |
668 | |
669 | local type = getXMLString(xmlFile, modKey.."#type") |
670 | local typeIndex = SoundModifierType[type] |
671 | if typeIndex ~= nil then |
672 | if modifier[typeIndex] == nil then |
673 | modifier[typeIndex] = AnimCurve.new(linearInterpolator1) |
674 | end |
675 | |
676 | local value = getXMLFloat(xmlFile, modKey.."#value") |
677 | local modifiedValue = getXMLFloat(xmlFile, modKey.."#modifiedValue") |
678 | modifier[typeIndex]:addKeyframe({modifiedValue, time=value}, xmlFile, modKey) |
679 | |
680 | modifier.hasModification = true |
681 | --TODO: remove debug comment and enable warning for everyone for next release |
682 | --#debug else |
683 | --#debug Logging.xmlWarning(xmlFile, "Unknown modifier type '%s' in '%s'\nAvailable types: %s", type, key, table.concatKeys(SoundModifierType, ", ")) |
684 | end |
685 | i = i + 1 |
686 | end |
687 | |
688 | modifier.currentValue = nil |
689 | sample.modifiers[attribute] = modifier |
690 | end |
691 | end |
698 | function SoundManager:loadRandomizationsFromXML(sample, xmlFile, key, baseDir) |
699 | sample.randomizationsIn = sample.randomizationsIn or {} |
700 | sample.randomizationsOut = sample.randomizationsOut or {} |
701 | |
702 | local i = 0 |
703 | while true do |
704 | local baseKey = string.format("%s.randomization(%d)", key, i) |
705 | if not hasXMLProperty(xmlFile, baseKey) then |
706 | break |
707 | end |
708 | |
709 | local randomization = {} |
710 | |
711 | randomization.minVolume = getXMLFloat(xmlFile, baseKey.."#minVolume") |
712 | randomization.maxVolume = getXMLFloat(xmlFile, baseKey.."#maxVolume") |
713 | |
714 | randomization.minPitch = getXMLFloat(xmlFile, baseKey.."#minPitch") |
715 | randomization.maxPitch = getXMLFloat(xmlFile, baseKey.."#maxPitch") |
716 | |
717 | randomization.minLowpassGain = getXMLFloat(xmlFile, baseKey.."#minLowpassGain") |
718 | randomization.maxLowpassGain = getXMLFloat(xmlFile, baseKey.."#maxLowpassGain") |
719 | |
720 | randomization.isInside = Utils.getNoNil(getXMLBool(xmlFile, baseKey.."#isInside"), true) |
721 | randomization.isOutside = Utils.getNoNil(getXMLBool(xmlFile, baseKey.."#isOutside"), true) |
722 | |
723 | if randomization.isInside then |
724 | if randomization.minVolume ~= nil then |
725 | if sample.indoorAttributes.volume + randomization.minVolume <= 0 then |
726 | Logging.xmlWarning(xmlFile, "Invalid sample '%s' randomization found in %s. randomization#minVolume can result in negative volume (indoor)", sample.templateName or sample.sampleName, baseKey) |
727 | end |
728 | end |
729 | |
730 | table.insert(sample.randomizationsIn, randomization) |
731 | end |
732 | |
733 | if randomization.isOutside then |
734 | if randomization.minVolume ~= nil then |
735 | if sample.outdoorAttributes.volume + randomization.minVolume <= 0 then |
736 | Logging.xmlWarning(xmlFile, "Invalid sample '%s' randomization found in %s. randomization#minVolume can result in negative volume (outdoor)", sample.templateName or sample.sampleName, baseKey) |
737 | end |
738 | end |
739 | |
740 | table.insert(sample.randomizationsOut, randomization) |
741 | end |
742 | |
743 | i = i + 1 |
744 | end |
745 | |
746 | sample.sourceRandomizations = sample.sourceRandomizations or {} |
747 | i = 0 |
748 | while true do |
749 | local baseKey = string.format("%s.sourceRandomization(%d)", key, i) |
750 | if not hasXMLProperty(xmlFile, baseKey) then |
751 | break |
752 | end |
753 | |
754 | local filename = getXMLString(xmlFile, baseKey.."#file") |
755 | if filename ~= nil then |
756 | if filename ~= "-" then |
757 | filename = Utils.getFilename(filename, baseDir) |
758 | end |
759 | |
760 | local randomSample = self:getRandomSample(sample, filename) |
761 | table.insert(sample.sourceRandomizations, randomSample) |
762 | end |
763 | |
764 | i = i + 1 |
765 | end |
766 | |
767 | if #sample.sourceRandomizations > 0 and not sample.addedBaseFileToRandomizations then |
768 | local filename = Utils.getFilename(sample.filename, baseDir) |
769 | |
770 | local randomSample = self:getRandomSample(sample, filename) |
771 | table.insert(sample.sourceRandomizations, randomSample) |
772 | |
773 | sample.addedBaseFileToRandomizations = true |
774 | end |
775 | end |
323 | function SoundManager:loadSample2DFromXML(xmlFile, baseKey, sampleName, baseDir, loops, audioGroup) |
324 | local sample = nil |
325 | |
326 | local isValid, usedExternal, definitionXmlFile, sampleKey = self:validateSampleDefinition(xmlFile, baseKey, sampleName, baseDir, audioGroup, true) |
327 | |
328 | if isValid then |
329 | sample = {} |
330 | sample.is2D = true |
331 | sample.sampleName = sampleName |
332 | |
333 | local template = getXMLString(definitionXmlFile, sampleKey .. "#template") |
334 | if template ~= nil then |
335 | sample = self:loadSampleAttributesFromTemplate(sample, template, baseDir, loops, definitionXmlFile, sampleKey) |
336 | end |
337 | |
338 | if not self:loadSampleAttributesFromXML(sample, definitionXmlFile, sampleKey, baseDir, loops) then |
339 | return nil |
340 | end |
341 | |
342 | sample.filename = Utils.getFilename(sample.filename, baseDir) |
343 | sample.linkNode = nil |
344 | sample.current = sample.outdoorAttributes |
345 | sample.audioGroup = audioGroup |
346 | sample.supportsReverb = Utils.getNoNil(getXMLBool(xmlFile, sampleKey.."#supportsReverb"), true) |
347 | |
348 | sample.soundSample = createSample(sample.sampleName) |
349 | sample.orgSoundSample = sample.soundSample |
350 | loadSample(sample.soundSample, sample.filename, false) -- false -> 2D sound |
351 | sample.duration = getSampleDuration(sample.soundSample) |
352 | |
353 | setSampleGroup(sample.soundSample, sample.audioGroup) |
354 | setSampleVolume(sample.soundSample, sample.current.volume) |
355 | setSamplePitch(sample.soundSample, sample.current.pitch) |
356 | setSampleFrequencyFilter(sample.soundSample, 1.0, sample.current.lowpassGain, 0.0, sample.current.lowpassCutoffFrequency, 0.0, sample.current.lowpassResonance) |
357 | if sample.supportsReverb then |
358 | addSampleEffect(sample.soundSample, SoundManager.DEFAULT_REVERB_EFFECT) |
359 | else |
360 | removeSampleEffect(sample.soundSample, SoundManager.DEFAULT_REVERB_EFFECT) |
361 | end |
362 | |
363 | sample.offsets = {volume=0, pitch=0, lowpassGain=0} |
364 | |
365 | self.samples[sample] = sample |
366 | table.insert(self.orderedSamples, sample) |
367 | end |
368 | |
369 | if usedExternal then |
370 | delete(definitionXmlFile) |
371 | end |
372 | |
373 | return sample |
374 | end |
571 | function SoundManager:loadSampleAttributesFromXML(sample, xmlFile, key, baseDir, defaultLoops, requiresFile) |
572 | local parent = getXMLString(xmlFile, key.."#parent") |
573 | |
574 | |
575 | if parent ~= nil then |
576 | local templateKey = self.soundTemplates[parent] |
577 | if templateKey ~= nil then |
578 | self:loadSampleAttributesFromXML(sample, self.soundTemplateXMLFile, templateKey, baseDir, defaultLoops, false) |
579 | end |
580 | end |
581 | |
582 | sample.filename = Utils.getNoNil(getXMLString(xmlFile, key.."#file"), sample.filename) |
583 | if sample.filename == nil and (requiresFile == nil or requiresFile) then |
584 | print("Warning: Filename not defined in '"..tostring(key).. "'. Ignoring it!") |
585 | return false |
586 | end |
587 | |
588 | sample.linkNodeOffset = Utils.getNoNil(getXMLString(xmlFile, key.."#linkNodeOffset"), "0 0 0"):getVectorN(3) |
589 | |
590 | sample.innerRadius = Utils.getNoNil(Utils.getNoNil(getXMLFloat(xmlFile, key.."#innerRadius"), sample.innerRadius), 5.0) |
591 | sample.outerRadius = Utils.getNoNil(Utils.getNoNil(getXMLFloat(xmlFile, key.."#outerRadius"), sample.outerRadius), 80.0) |
592 | |
593 | sample.volumeScale = Utils.getNoNil(Utils.getNoNil(getXMLFloat(xmlFile, key.."#volumeScale"), sample.volumeScale), 1.0) |
594 | sample.pitchScale = Utils.getNoNil(Utils.getNoNil(getXMLFloat(xmlFile, key.."#pitchScale"), sample.pitchScale), 1.0) |
595 | sample.lowpassGainScale = Utils.getNoNil(Utils.getNoNil(getXMLFloat(xmlFile, key.."#lowpassGainScale"), sample.lowpassGainScale), 1.0) |
596 | |
597 | sample.loopSynthesisRPMRatio = Utils.getNoNil(Utils.getNoNil(getXMLFloat(xmlFile, key.."#loopSynthesisRPMRatio"), sample.loopSynthesisRPMRatio), 1.0) |
598 | |
599 | sample.indoorAttributes = Utils.getNoNil(sample.indoorAttributes, {}) |
600 | sample.indoorAttributes.volume = Utils.getNoNil(Utils.getNoNil(getXMLFloat(xmlFile, key..".volume#indoor"), sample.indoorAttributes.volume), 0.8) |
601 | sample.indoorAttributes.pitch = Utils.getNoNil(Utils.getNoNil(getXMLFloat(xmlFile, key..".pitch#indoor"), sample.indoorAttributes.pitch), 1.0) |
602 | sample.indoorAttributes.lowpassGain = Utils.getNoNil(Utils.getNoNil(getXMLFloat(xmlFile, key..".lowpassGain#indoor"), sample.indoorAttributes.lowpassGain), 0.8) |
603 | sample.indoorAttributes.lowpassCutoffFrequency = Utils.getNoNil(Utils.getNoNil(getXMLFloat(xmlFile, key..".lowpassCutoffFrequency#indoor"), sample.indoorAttributes.lowpassCutoffFrequency), 0.0) -- by default this is defined by the group (default 5000hz, resonance 2) |
604 | sample.indoorAttributes.lowpassResonance = Utils.getNoNil(Utils.getNoNil(getXMLFloat(xmlFile, key..".lowpassResonance#indoor"), sample.indoorAttributes.lowpassResonance), 0.0) |
605 | |
606 | sample.outdoorAttributes = Utils.getNoNil(sample.outdoorAttributes, {}) |
607 | sample.outdoorAttributes.volume = Utils.getNoNil(Utils.getNoNil(getXMLFloat(xmlFile, key..".volume#outdoor"), sample.outdoorAttributes.volume), 1.0) |
608 | sample.outdoorAttributes.pitch = Utils.getNoNil(Utils.getNoNil(getXMLFloat(xmlFile, key..".pitch#outdoor"), sample.outdoorAttributes.pitch), 1.0) |
609 | sample.outdoorAttributes.lowpassGain = Utils.getNoNil(Utils.getNoNil(getXMLFloat(xmlFile, key..".lowpassGain#outdoor"), sample.outdoorAttributes.lowpassGain), 1.0) |
610 | sample.outdoorAttributes.lowpassCutoffFrequency = Utils.getNoNil(Utils.getNoNil(getXMLFloat(xmlFile, key..".lowpassCutoffFrequency#outdoor"), sample.outdoorAttributes.lowpassCutoffFrequency), 0.0) |
611 | sample.outdoorAttributes.lowpassResonance = Utils.getNoNil(Utils.getNoNil(getXMLFloat(xmlFile, key..".lowpassResonance#outdoor"), sample.outdoorAttributes.lowpassResonance), 0.0) |
612 | |
613 | sample.loops = Utils.getNoNil(Utils.getNoNil(getXMLInt(xmlFile, key.."#loops"), sample.loops), Utils.getNoNil(defaultLoops, 1)) |
614 | |
615 | sample.supportsReverb = Utils.getNoNil(Utils.getNoNil(getXMLBool(xmlFile, key.."#supportsReverb"), sample.supportsReverb), true) |
616 | |
617 | sample.isLocalSound = Utils.getNoNil(Utils.getNoNil(getXMLBool(xmlFile, key.."#isLocalSound"), sample.isLocalSound), false) |
618 | |
619 | sample.debug = Utils.getNoNil(Utils.getNoNil(getXMLBool(xmlFile, key.."#debug"), sample.debug), false) |
620 | if sample.debug or SoundManager.GLOBAL_DEBUG_ENABLED then |
621 | if sample.debug then |
622 | -- save flagged samples in separate table to keep them when disabling global mode |
623 | table.insert(self.debugSamplesFlagged, sample) |
624 | end |
625 | self.debugSamples[sample] = true |
626 | sample.debug = nil |
627 | end |
628 | |
629 | local fadeIn = getXMLFloat(xmlFile, key.."#fadeIn") |
630 | if fadeIn ~= nil then |
631 | fadeIn = fadeIn * 1000 |
632 | end |
633 | sample.fadeIn = Utils.getNoNil(Utils.getNoNil(fadeIn, sample.fadeIn), 0) |
634 | |
635 | local fadeOut = getXMLFloat(xmlFile, key.."#fadeOut") |
636 | if fadeOut ~= nil then |
637 | fadeOut = fadeOut * 1000 |
638 | end |
639 | sample.fadeOut = Utils.getNoNil(Utils.getNoNil(fadeOut, sample.fadeOut), 0) |
640 | |
641 | sample.fade = 0 |
642 | |
643 | sample.isIndoor = false |
644 | |
645 | self:loadModifiersFromXML(sample, xmlFile, key) |
646 | self:loadRandomizationsFromXML(sample, xmlFile, key, baseDir) |
647 | |
648 | return true |
649 | end |
387 | function SoundManager:loadSampleFromXML(xmlFile, baseKey, sampleName, baseDir, components, loops, audioGroup, i3dMappings, modifierTargetObject) |
388 | local sample = nil |
389 | |
390 | if type(xmlFile) == "table" then |
391 | xmlFile = xmlFile.handle |
392 | end |
393 | |
394 | local externalSoundsFile, volumeFactor |
395 | if modifierTargetObject ~= nil then |
396 | externalSoundsFile = modifierTargetObject.externalSoundsFile |
397 | volumeFactor = modifierTargetObject.soundVolumeFactor |
398 | end |
399 | |
400 | local isValid, _, definitionXmlFile, sampleKey, linkNode = self:validateSampleDefinition( |
401 | xmlFile, baseKey, sampleName, baseDir, audioGroup, false, components, i3dMappings, externalSoundsFile) |
402 | |
403 | if isValid then |
404 | sample = {} |
405 | sample.is2D = false |
406 | sample.sampleName = sampleName |
407 | |
408 | local template = getXMLString(definitionXmlFile, sampleKey .. "#template") |
409 | if template ~= nil then |
410 | sample = self:loadSampleAttributesFromTemplate(sample, template, baseDir, loops, definitionXmlFile, sampleKey) |
411 | end |
412 | |
413 | if not self:loadSampleAttributesFromXML(sample, definitionXmlFile, sampleKey, baseDir, loops) then |
414 | return nil |
415 | end |
416 | |
417 | sample.filename = Utils.getFilename(sample.filename, baseDir) |
418 | sample.isGlsFile = sample.filename:find(".gls") ~= nil |
419 | sample.linkNode = linkNode |
420 | sample.modifierTargetObject = modifierTargetObject |
421 | sample.current = sample.outdoorAttributes |
422 | sample.audioGroup = audioGroup |
423 | |
424 | if volumeFactor ~= nil then |
425 | sample.volumeScale = sample.volumeScale * volumeFactor |
426 | end |
427 | |
428 | self:createAudioSource(sample, sample.filename) |
429 | |
430 | sample.offsets = {volume=0, pitch=0, lowpassGain=0} |
431 | |
432 | self.samples[sample] = sample |
433 | table.insert(self.orderedSamples, sample) |
434 | end |
435 | |
436 | return sample |
437 | end |
483 | function SoundManager:onCreateAudioSource(sample, ignoreReverb) |
484 | sample.soundSample = getAudioSourceSample(sample.soundNode) |
485 | sample.duration = getSampleDuration(sample.soundSample) |
486 | sample.outerRange = getAudioSourceRange(sample.soundNode) |
487 | sample.innerRange = getAudioSourceInnerRange(sample.soundNode) |
488 | sample.isDirty = true |
489 | |
490 | setSampleGroup(sample.soundSample, sample.audioGroup) |
491 | setSampleVolume(sample.soundSample, sample.current.volume) |
492 | setSamplePitch(sample.soundSample, sample.current.pitch) |
493 | setSampleFrequencyFilter(sample.soundSample, 1.0, sample.current.lowpassGain, 0.0, sample.current.lowpassCutoffFrequency, 0.0, sample.current.lowpassResonance) |
494 | |
495 | if not ignoreReverb then |
496 | if sample.supportsReverb then |
497 | addSampleEffect(sample.soundSample, SoundManager.DEFAULT_REVERB_EFFECT) |
498 | else |
499 | removeSampleEffect(sample.soundSample, SoundManager.DEFAULT_REVERB_EFFECT) |
500 | end |
501 | end |
502 | |
503 | setAudioSourceAutoPlay(sample.soundNode, false) |
504 | |
505 | link(sample.linkNode, sample.soundNode) |
506 | |
507 | if sample.linkNodeOffset == nil then |
508 | setTranslation(sample.soundNode, 0, 0, 0) |
509 | else |
510 | setTranslation(sample.soundNode, sample.linkNodeOffset[1], sample.linkNodeOffset[2], sample.linkNodeOffset[3]) |
511 | end |
512 | end |
1405 | function SoundManager.registerSampleXMLPaths(schema, basePath, name) |
1406 | schema:setSubSchemaIdentifier("sounds") |
1407 | |
1408 | if name == nil then |
1409 | Logging.error("Failed to register sound sample xml paths! No sound name given.") |
1410 | printCallstack() |
1411 | end |
1412 | |
1413 | schema:setXMLSharedRegistration("SoundManager_sound", basePath) |
1414 | |
1415 | local soundPath = basePath .. "." .. name |
1416 | schema:register(XMLValueType.NODE_INDEX, soundPath .. "#linkNode", "Link node for 3d sound") |
1417 | schema:register(XMLValueType.VECTOR_TRANS, soundPath .. "#linkNodeOffset", "Sound source will be offset by this value to the link node") |
1418 | schema:register(XMLValueType.STRING, soundPath .. "#template", "Sound template name") |
1419 | schema:register(XMLValueType.STRING, soundPath .. "#parent", "Parent sample for heredity") |
1420 | schema:register(XMLValueType.STRING, soundPath .. "#file", "Path to sound sample") |
1421 | schema:register(XMLValueType.FLOAT, soundPath .. "#outerRadius", "Outer radius", 5) |
1422 | schema:register(XMLValueType.FLOAT, soundPath .. "#innerRadius", "Inner radius", 80) |
1423 | schema:register(XMLValueType.INT, soundPath .. "#loops", "Number of loops (0 = infinite)", 1) |
1424 | schema:register(XMLValueType.BOOL, soundPath .. "#supportsReverb", "Flag to disable reverb", true) |
1425 | schema:register(XMLValueType.BOOL, soundPath .. "#isLocalSound", "While set for vehicle sounds it will only play for the player currently using the vehicle", false) |
1426 | schema:register(XMLValueType.BOOL, soundPath .. "#debug", "Flag to enable debug rendering", false) |
1427 | schema:register(XMLValueType.FLOAT, soundPath .. "#fadeIn", "Fade in time in seconds", 0) |
1428 | schema:register(XMLValueType.FLOAT, soundPath .. "#fadeOut", "Fade out time in seconds", 0) |
1429 | |
1430 | schema:register(XMLValueType.FLOAT, soundPath .. ".volume#indoor", "Indoor volume", 0.8) |
1431 | schema:register(XMLValueType.FLOAT, soundPath .. ".pitch#indoor", "Indoor pitch", 1.0) |
1432 | schema:register(XMLValueType.FLOAT, soundPath .. ".lowpassGain#indoor", "Indoor lowpass gain", 0.8) |
1433 | |
1434 | schema:register(XMLValueType.FLOAT, soundPath .. ".lowpassCutoffFrequency#indoor", "Indoor lowpass cutoff frequency", 5000.0) |
1435 | schema:register(XMLValueType.FLOAT, soundPath .. ".lowpassResonance#indoor", "Indoor lowpass resonance", 2.0) |
1436 | schema:register(XMLValueType.FLOAT, soundPath .. ".lowpassCutoffFrequency#outdoor", "Outdoor lowpass cutoff frequency", 5000.0) |
1437 | schema:register(XMLValueType.FLOAT, soundPath .. ".lowpassResonance#outdoor", "Outdoor lowpass resonance", 2.0) |
1438 | |
1439 | schema:register(XMLValueType.FLOAT, soundPath .. ".volume#outdoor", "Outdoor volume", 1.0) |
1440 | schema:register(XMLValueType.FLOAT, soundPath .. ".pitch#outdoor", "Outdoor pitch", 1.0) |
1441 | schema:register(XMLValueType.FLOAT, soundPath .. ".lowpassGain#outdoor", "Outdoor lowpass gain", 1.0) |
1442 | |
1443 | schema:register(XMLValueType.FLOAT, soundPath .. "#volumeScale", "Additional scale that is applied on the volume attributes", 1) |
1444 | schema:register(XMLValueType.FLOAT, soundPath .. "#pitchScale", "Additional pitch that is applied on the volume attributes", 1) |
1445 | schema:register(XMLValueType.FLOAT, soundPath .. "#lowpassGainScale", "Additional lowpass gain that is applied on the volume attributes", 1) |
1446 | |
1447 | schema:register(XMLValueType.FLOAT, soundPath .. "#loopSynthesisRPMRatio", "Ratio between rpm in the gls file and actual rpm of the motor (e.g. 0.9: max. rpm in the gls file will be reached at 90% of motor rpm)", 1) |
1448 | |
1449 | SoundManager.registerModifierXMLPaths(schema, soundPath .. ".volume") |
1450 | SoundManager.registerModifierXMLPaths(schema, soundPath .. ".pitch") |
1451 | SoundManager.registerModifierXMLPaths(schema, soundPath .. ".lowpassGain") |
1452 | SoundManager.registerModifierXMLPaths(schema, soundPath .. ".loopSynthesisRpm") |
1453 | SoundManager.registerModifierXMLPaths(schema, soundPath .. ".loopSynthesisLoad") |
1454 | |
1455 | schema:register(XMLValueType.FLOAT, soundPath .. ".randomization(?)#minVolume", "Min volume") |
1456 | schema:register(XMLValueType.FLOAT, soundPath .. ".randomization(?)#maxVolume", "Max volume") |
1457 | schema:register(XMLValueType.FLOAT, soundPath .. ".randomization(?)#minPitch", "Max pitch") |
1458 | schema:register(XMLValueType.FLOAT, soundPath .. ".randomization(?)#maxPitch", "Max pitch") |
1459 | schema:register(XMLValueType.FLOAT, soundPath .. ".randomization(?)#minLowpassGain", "Max lowpass gain") |
1460 | schema:register(XMLValueType.FLOAT, soundPath .. ".randomization(?)#maxLowpassGain", "Max lowpass gain") |
1461 | schema:register(XMLValueType.BOOL, soundPath .. ".randomization(?)#isInside", "Randomization is applied inside", true) |
1462 | schema:register(XMLValueType.BOOL, soundPath .. ".randomization(?)#isOutside", "Randomization is applied outside", true) |
1463 | |
1464 | schema:register(XMLValueType.STRING, soundPath .. ".sourceRandomization(?)#file", "Path to sound sample") |
1465 | |
1466 | schema:setXMLSharedRegistration() |
1467 | schema:setSubSchemaIdentifier() |
1468 | end |
810 | function SoundManager:update(dt) |
811 | for i=0, SoundManager.MAX_SAMPLES_PER_FRAME do |
812 | local index = self.currentSampleIndex |
813 | |
814 | if index > #self.activeSamples then |
815 | self.currentSampleIndex = 1 |
816 | break |
817 | end |
818 | |
819 | local sample = self.activeSamples[index] |
820 | if self:getIsSamplePlaying(sample) then |
821 | self:updateSampleFade(sample, dt) |
822 | self:updateSampleModifiers(sample) |
823 | self:updateSampleAttributes(sample) |
824 | else |
825 | table.removeElement(self.activeSamples, sample) |
826 | sample.fade = 0 |
827 | end |
828 | |
829 | self.currentSampleIndex = self.currentSampleIndex + 1 |
830 | end |
831 | |
832 | -- collect samples to debug |
833 | table.clear(self.debugSamplesLinkNodes) |
834 | for sample in pairs(self.debugSamples) do |
835 | if sample.linkNode ~= nil and entityExists(sample.linkNode) then |
836 | local distanceToCam = calcDistanceFrom(getCamera(), sample.linkNode) |
837 | -- sample is flagged for debugging or global debug is enabled + player is within 15m or 150% of the outer radius of the source |
838 | if distanceToCam < 15 or (distanceToCam < sample.outerRadius * 1.5) then |
839 | -- group samples by linkNodes |
840 | if self.debugSamplesLinkNodes[sample.linkNode] == nil then |
841 | self.debugSamplesLinkNodes[sample.linkNode] = {} |
842 | end |
843 | table.insert(self.debugSamplesLinkNodes[sample.linkNode], sample) |
844 | end |
845 | end |
846 | end |
847 | end |
925 | function SoundManager:updateSampleAttributes(sample, force) |
926 | if sample ~= nil then |
927 | if sample.isIndoor ~= self.isIndoor or force then |
928 | self:setCurrentSampleAttributes(sample, self.isIndoor) |
929 | sample.isIndoor = self.isIndoor |
930 | end |
931 | |
932 | local volumeFactor = self:getModifierFactor(sample, "volume") |
933 | local pitchFactor = self:getModifierFactor(sample, "pitch") |
934 | local lowpassGainFactor = self:getModifierFactor(sample, "lowpassGain") |
935 | |
936 | setSampleVolume(sample.soundSample, volumeFactor * self:getCurrentSampleVolume(sample)) |
937 | setSamplePitch(sample.soundSample, pitchFactor * self:getCurrentSamplePitch(sample)) |
938 | setSampleFrequencyFilter(sample.soundSample, 1.0, lowpassGainFactor * self:getCurrentSampleLowpassGain(sample), 0.0, sample.current.lowpassCutoffFrequency, 0.0, sample.current.lowpassResonance) |
939 | |
940 | if sample.modifiers["loopSynthesisRpm"].hasModification then |
941 | local loopSynthesisRpmFactor = self:getModifierFactor(sample, "loopSynthesisRpm") |
942 | setSampleLoopSynthesisRPM(sample.soundSample, MathUtil.clamp(loopSynthesisRpmFactor, 0, 1), true) |
943 | end |
944 | |
945 | if sample.modifiers["loopSynthesisLoad"].hasModification then |
946 | local loopSynthesisLoadFactor = self:getModifierFactor(sample, "loopSynthesisLoad") |
947 | setSampleLoopSynthesisLoadFactor(sample.soundSample, MathUtil.clamp(loopSynthesisLoadFactor, 0, 1)) |
948 | end |
949 | end |
950 | end |
956 | function SoundManager:updateSampleRandomizations(sample) |
957 | if sample ~= nil then |
958 | for _, name in ipairs(SoundManager.SAMPLE_RANDOMIZATIONS) do |
959 | if (name == "randomizationsIn") == sample.isIndoor then |
960 | local numRandomizations = #sample[name] |
961 | if numRandomizations > 0 then |
962 | local randomizationIndexToUse = math.max(math.floor(math.random(numRandomizations)), 1) |
963 | local randomizationToUse = sample[name][randomizationIndexToUse] |
964 | |
965 | if randomizationToUse.minVolume ~= nil and randomizationToUse.maxVolume then |
966 | sample[name].volume = math.random() * (randomizationToUse.maxVolume - randomizationToUse.minVolume) + randomizationToUse.minVolume |
967 | end |
968 | |
969 | if randomizationToUse.minPitch ~= nil and randomizationToUse.maxPitch then |
970 | sample[name].pitch = math.random() * (randomizationToUse.maxPitch - randomizationToUse.minPitch) + randomizationToUse.minPitch |
971 | end |
972 | |
973 | if randomizationToUse.minLowpassGain ~= nil and randomizationToUse.maxLowpassGain then |
974 | sample[name].lowpassGain = math.random() * (randomizationToUse.maxLowpassGain - randomizationToUse.minLowpassGain) + randomizationToUse.minLowpassGain |
975 | end |
976 | end |
977 | end |
978 | end |
979 | |
980 | local numRandomizations = #sample.sourceRandomizations |
981 | if numRandomizations > 0 then |
982 | local randomizationIndexToUse = 1 |
983 | for i = 1, 3 do |
984 | randomizationIndexToUse = math.max(math.floor(math.random(numRandomizations)), 1) |
985 | if self.oldRandomizationIndex ~= randomizationIndexToUse then |
986 | break |
987 | end |
988 | end |
989 | self.oldRandomizationIndex = randomizationIndexToUse |
990 | local randomSample = sample.sourceRandomizations[randomizationIndexToUse] |
991 | |
992 | if not sample.is2D then |
993 | if sample.soundSample ~= nil then |
994 | stopSample(sample.soundSample, 0.0, sample.fadeOut) |
995 | end |
996 | |
997 | if not randomSample.isEmpty then |
998 | sample.soundNode = randomSample.soundNode |
999 | self:onCreateAudioSource(sample, true) |
1000 | sample.isEmptySample = false |
1001 | else |
1002 | sample.isEmptySample = true |
1003 | end |
1004 | else |
1005 | if sample.soundSample ~= nil then |
1006 | stopSample(sample.soundSample, 0.0, sample.fadeOut) |
1007 | end |
1008 | |
1009 | if not randomSample.isEmpty then |
1010 | sample.soundSample = randomSample.soundSample |
1011 | self:onCreateAudio2d(sample) |
1012 | sample.isEmptySample = false |
1013 | else |
1014 | sample.isEmptySample = true |
1015 | end |
1016 | end |
1017 | end |
1018 | end |
1019 | end |
261 | function SoundManager:validateSampleDefinition(xmlFile, baseKey, sampleName, baseDir, audioGroup, is2D, components, i3dMappings, externalSoundsFile) |
262 | local isValid = false |
263 | local usedExternal = false |
264 | local actualXMLFile = xmlFile |
265 | local sampleKey = "" |
266 | local linkNode = nil |
267 | |
268 | if sampleName ~= nil then |
269 | if not AudioGroup.getIsValidAudioGroup(audioGroup) then |
270 | print("Warning: Invalid audioGroup index '" .. tostring(audioGroup) .. "'.") |
271 | end |
272 | |
273 | sampleKey = baseKey .. "." .. sampleName |
274 | |
275 | if externalSoundsFile ~= nil then |
276 | if not hasXMLProperty(actualXMLFile, sampleKey) then |
277 | sampleKey = Vehicle.xmlSchemaSounds:replaceRootName(sampleKey) |
278 | actualXMLFile = externalSoundsFile.handle |
279 | usedExternal = true |
280 | end |
281 | end |
282 | |
283 | local xmlFileObject = g_xmlManager:getFileByHandle(xmlFile) |
284 | if xmlFileObject ~= nil then |
285 | XMLUtil.checkDeprecatedXMLElements(xmlFileObject, baseKey .. "#externalSoundFile", "vehicle.base.sounds#filename") --FS19 to FS22 |
286 | end |
287 | |
288 | if actualXMLFile ~= nil then |
289 | if hasXMLProperty(actualXMLFile, sampleKey) then |
290 | isValid = true |
291 | |
292 | if not is2D then -- check if linkNode exists |
293 | linkNode = I3DUtil.indexToObject(components, getXMLString(actualXMLFile, sampleKey .. "#linkNode"), i3dMappings) |
294 | if linkNode == nil then |
295 | if type(components) == "number" then |
296 | linkNode = components |
297 | elseif type(components) == "table" then |
298 | linkNode = components[1].node |
299 | else |
300 | print("Warning: Could not find linkNode (" .. tostring(getXMLString(actualXMLFile, sampleKey .. "#linkNode")) .. ") for sample '" .. tostring(sampleName) .. "'. Ignoring it!") |
301 | isValid = false |
302 | end |
303 | end |
304 | end |
305 | end |
306 | else |
307 | Logging.warning("Unable to load sample '%s' from internal or given external sound file '%s'!", sampleName, externalSoundsFile) |
308 | end |
309 | end |
310 | |
311 | return isValid, usedExternal, actualXMLFile, sampleKey, linkNode |
312 | end |