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.