Jirdus
Topic Author
Posts: 2
Joined: 01 Feb 2022, 20:53

Audio position in Unity

10 Feb 2022, 10:21

Is it possible to somehow change the behavior in Unity when AudioClips are played fixed to the world position of Vector3.zero? For example, use the current camera position instead?
If the camera moves or is in a different position than Vector3.zero, the panning and volume of the AudioClip changes depending on the camera position to Vector3.zero.
Is PlaySoundAction played using PlayAudio method in NoesisProviders.cs?
public void PlayAudio(string uri, float volume)
    {
        Value v;
        if (_audios.TryGetValue(uri, out v) && v.audio != null)
        {
            UnityEngine.AudioSource.PlayClipAtPoint(v.audio, UnityEngine.Vector3.zero, volume);
        }
        else
        {
            UnityEngine.Debug.LogError("AudioClip not found '" + uri + "'");
        }
    }
 
User avatar
jsantos
Site Admin
Posts: 4822
Joined: 20 Jan 2012, 17:18
Contact:

Re: Audio position in Unity

14 Feb 2022, 15:15

I propose adding a new PlayAudio function to NoesisProviders.cs (we can add this to noesis, please report a ticket about it)
public void PlayAudio(string uri, UnityEngine.Vector3 position, float volume)
{
    Value v;
    if (_audios.TryGetValue(uri, out v) && v.audio != null)
    {
        UnityEngine.AudioSource.PlayClipAtPoint(v.audio, position, volume);
    }
    else
    {
        UnityEngine.Debug.LogError("AudioClip not found '" + uri + "'");
    }
}
And then, I recommend creating a new action PlayPositionalSoundAction (cloning from PlaySoundAction) and adding a new dependency property for the Position and implementing Invoke as:
protected override void Invoke(object parameter)
{
    Uri source = Source;
    if (AssociatedObject != null && source != null &&
        !string.IsNullOrEmpty(source.OriginalString))
    {
        AudioProvider.instance.PlayAudio(uri.GetPath(), Position, Volume);
    }
}
And now, you have a Position property in XAML that you can bind to your data context and use whatever value you need (camera position for example).

Please, let us know if this works for you and let's see if we can include a similar solution in Noesis.
 
Jirdus
Topic Author
Posts: 2
Joined: 01 Feb 2022, 20:53

Re: Audio position in Unity

23 Feb 2022, 12:37

Thank you for your solution, it works as expected. Below are the steps, if anyone wants to implement this.

It's a good universal solution, after some modifications it could be used so that the sound comes directly from the individual buttons in 3D.

However, I think for most cases it will be enough if the sound always comes from one place, and I expected that to be the default behavior as well.
If the NoesisView component is attached to a camera that is moving, now it doesn't work as expected.

So I decided to use a simpler solution and that is to modify the AudioSource.PlayClipAtPoint method, which actually just creates a new temporary GameObject with the AudioSource component, which sets the spatialBlend property to 1.0, which means that the audio will be played in 3D. For my needs, I just set this property to zero. So, if Unity decided to allow this property to be set as a parameter in PlayClipAtPoint, everything would be easier. I decided to modify the PlayClipAtPoint method and also use pooling for temporary AudioSource objects. You can find the code below as well.

PlayPositionalSoundAction solution:

1. Add PlayAudio method overloading in the NoesisProviders class:
public void PlayAudio(string uri, UnityEngine.Vector3 position, float volume)
{
    if (_audios.TryGetValue(uri, out var v) && v.audio != null)
    {
        UnityEngine.AudioSource.PlayClipAtPoint(v.audio, position, volume);
    }
    else
    {
        UnityEngine.Debug.LogError("AudioClip not found '" + uri + "'");
    }
}
2. Add to the DataContext (my ViewModel inherits from MonoBehaviour):

[SerializeField]
private UnityEngine.Transform _uiSoundPositionObject;

public Vector3 SoundPosition => _uiSoundPositionObject != null ? _uiSoundPositionObject.position : Vector3.zero;

private void Update()
{
    if (_uiSoundPositionObject != null)
    {
        OnPropertyChanged(nameof(SoundPosition));
    }
 }
3. Add the PlayPositionalSoundAction class to the Assets folder
#if UNITY_5_3_OR_NEWER
#define NOESIS
using UnityEngine;
using Noesis;
using NoesisApp;
#else
using Microsoft.Xaml.Behaviors;
using System.Numerics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
#endif
using System;

namespace YourNamespace
{
    public class PlayPositionalSoundAction : TriggerAction<DependencyObject>
    {
#if NOESIS
        public new PlayPositionalSoundAction Clone()
        {
            return (PlayPositionalSoundAction)base.Clone();
        }

        public new PlayPositionalSoundAction CloneCurrentValue()
        {
            return (PlayPositionalSoundAction)base.CloneCurrentValue();
        }

        protected override void Invoke(object parameter)
        {
            var source = Source;
            if (AssociatedObject != null && source != null &&
                !string.IsNullOrEmpty(source.OriginalString))
            {
                AudioProvider.instance.PlayAudio(source.GetPath(), Position, (float)Volume);
            }
        }
#else
        public PlayPositionalSoundAction()
        {
        }

        protected virtual void SetMediaElementProperties(MediaElement mediaElement)
        {
            if (mediaElement != null)
            {
                mediaElement.Source = Source;
                mediaElement.Volume = Volume;
            }
        }

        protected override void Invoke(object parameter)
        {
            if (!(Source == null) && base.AssociatedObject != null)
            {
                Popup popup = new Popup();
                MediaElement mediaElement = new MediaElement();
                popup.Child = mediaElement;
                mediaElement.Visibility = Visibility.Collapsed;
                SetMediaElementProperties(mediaElement);
                mediaElement.MediaEnded += delegate
                {
                    popup.Child = null;
                    popup.IsOpen = false;
                };
                mediaElement.MediaFailed += delegate
                {
                    popup.Child = null;
                    popup.IsOpen = false;
                };
                popup.IsOpen = true;
            }
        }
#endif
        public static readonly DependencyProperty PositionProperty = DependencyProperty.Register("Position",
            typeof(Vector3), typeof(PlayPositionalSoundAction), new PropertyMetadata(default(Vector3)));

        public static readonly DependencyProperty SourceProperty =
            DependencyProperty.Register("Source", typeof(Uri), typeof(PlayPositionalSoundAction), null);

        public static readonly DependencyProperty VolumeProperty = DependencyProperty.Register("Volume", typeof(double),
            typeof(PlayPositionalSoundAction), new PropertyMetadata(0.5));

        public Vector3 Position
        {
            get => (Vector3)GetValue(PositionProperty);
            set => SetValue(PositionProperty, value);
        }

        public Uri Source
        {
            get => (Uri)GetValue(SourceProperty);
            set => SetValue(SourceProperty, value);
        }

        public double Volume
        {
            get => (double)GetValue(VolumeProperty);
            set => SetValue(VolumeProperty, value);
        }
    }
}
4. Use in xaml:
<local:PlayPositionalSoundAction Source="/Assets/Sounds/ButtonHoverSound.wav" Volume="1" Position="{Binding SoundPosition, Mode=OneWay}"/>
5. In the Unity inspector, assign a camera object (or other as needed) to the uiSoundPositionObject property

Solution using the PlayClip custom method

1. Create a PlayClip method in the NoesisProviders class
    public static void PlayClip(AudioClip clip, [DefaultValue("1.0F")] float volume)
    {
        GameObject gameObject = new GameObject("One shot audio");
        var audioSource = gameObject.AddComponent<AudioSource>();
        audioSource.clip = clip;
        audioSource.volume = volume;
        audioSource.bypassEffects = true;
        audioSource.bypassListenerEffects = true;
        audioSource.bypassReverbZones = true;
        audioSource.spatialize = false;
        audioSource.spatialBlend = 0f;
        audioSource.Play();
        
        UnityEngine.Object.Destroy(audioSource.gameObject, clip.length * (Time.timeScale < 0.00999999977648258 ? 0.01f : Time.timeScale));
    }
2. Edit PlayAudio method
    public void PlayAudio(string uri, float volume)
    {
        if (_audios.TryGetValue(uri, out var v) && v.audio != null)
        {
            // UnityEngine.AudioSource.PlayClipAtPoint(v.audio, UnityEngine.Vector3.zero, volume);
            PlayClip(v.audio, volume);
        }
        else
        {
            UnityEngine.Debug.LogError("AudioClip not found '" + uri + "'");
        }
    }
Solution using custom PlayClip method and AudioSource object pooling

1. Insert the following code into the NoesisProviders class
    private static NoesisView _noesisView;
    private static NoesisView noesisView => _noesisView ??= UnityEngine.Object.FindObjectOfType<NoesisView>();
    
    public static void PlayClip(AudioClip clip, [DefaultValue("1.0F")] float volume)
    {
        var audioSource = AudioSourcePool.Obtain();
        audioSource.clip = clip;
        audioSource.volume = volume;
        audioSource.bypassEffects = true;
        audioSource.bypassListenerEffects = true;
        audioSource.bypassReverbZones = true;
        audioSource.spatialize = false;
        audioSource.spatialBlend = 0f;
        audioSource.Play();
        
        noesisView.StartCoroutine(FreeAudioSource(audioSource,
            clip.length * ((double)Time.timeScale < 0.00999999977648258 ? 0.01f : Time.timeScale)));
    }
    
    private static IEnumerator FreeAudioSource(AudioSource audioSource, float delay)
    {
        yield return new WaitForSeconds(delay);
        AudioSourcePool.Put(audioSource);
    }
    
    internal static class AudioSourcePool
    {
        private static readonly Queue<AudioSource> audioSources = new Queue<AudioSource>();
        public static int maxSize = 3;

        public static AudioSource Obtain()
        {
            var audioSource = audioSources.Count > 0 ? audioSources.Dequeue() : NewAudioSource();
            audioSource.gameObject.SetActive(true);
            return audioSource;
        }

        private static AudioSource NewAudioSource()
        {
            var gameObject = new GameObject("One shot audio");
            var audioSource = gameObject.AddComponent<AudioSource>();
            return audioSource;
        }

        public static void Put(AudioSource audioSource)
        {
            if (audioSource == null) return;
            audioSource.gameObject.SetActive(false);
            if (audioSources.Count >= maxSize)
            {
                UnityEngine.Object.Destroy(audioSource.gameObject);
            }
            else
            {
                audioSources.Enqueue(audioSource);
            }
        }
    }
2. Edit PlayAudio method
    public void PlayAudio(string uri, float volume)
    {
        if (_audios.TryGetValue(uri, out var v) && v.audio != null)
        {
            // UnityEngine.AudioSource.PlayClipAtPoint(v.audio, UnityEngine.Vector3.zero, volume);
            PlayClip(v.audio, volume);
        }
        else
        {
            UnityEngine.Debug.LogError("AudioClip not found '" + uri + "'");
        }
    }
I think that the PlayClip method could be the default solution in NoesisGUI. The playback of sounds in the menu should be the same wherever the camera is.

In case a specific sound needs to be played in 3D (for example if the NoesisView is like a GameObject in a scene), I would just use PlayPositionalSoundAction, maybe named PlaySoundAction3D.

All the solutions listed work well, I hope this helps someone.
 
User avatar
jsantos
Site Admin
Posts: 4822
Joined: 20 Jan 2012, 17:18
Contact:

Re: Audio position in Unity

23 Feb 2022, 18:56

Thanks for the time posting this! Good work there.

Who is online

Users browsing this forum: No registered users and 3 guests