Working on native DynamicSoundEffectInstance implementation in XAudio and OpenAL
This commit is contained in:
parent
0417857fd9
commit
3e65589f23
@ -33,7 +33,7 @@ namespace ANX.Framework.Audio
|
||||
this.sampleRate = sampleRate;
|
||||
this.channels = channels;
|
||||
var creator = AddInSystemFactory.Instance.GetDefaultCreator<ISoundSystemCreator>();
|
||||
nativeDynamicInstance = creator.CreateDynamicSoundEffectInstance();
|
||||
nativeDynamicInstance = creator.CreateDynamicSoundEffectInstance(sampleRate, channels);
|
||||
nativeDynamicInstance.BufferNeeded += OnBufferNeeded;
|
||||
SetNativeInstance(nativeDynamicInstance);
|
||||
}
|
||||
|
@ -38,6 +38,6 @@ namespace ANX.Framework.NonXNA.SoundSystem
|
||||
ISong CreateSong(Song parentSong, Uri uri);
|
||||
ISong CreateSong(Song parentSong, string filepath, int duration);
|
||||
|
||||
IDynamicSoundEffectInstance CreateDynamicSoundEffectInstance();
|
||||
IDynamicSoundEffectInstance CreateDynamicSoundEffectInstance(int sampleRate, AudioChannels channels);
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,6 @@ using System.Runtime.InteropServices;
|
||||
[assembly: InternalsVisibleTo("ANX.PlatformSystem.Metro")]
|
||||
[assembly: InternalsVisibleTo("ANX.PlatformSystem.PsVita")]
|
||||
[assembly: InternalsVisibleTo("ANX.SoundSystem.Windows.XAudio")]
|
||||
[assembly: InternalsVisibleTo("ANX.SoundSystem.Windows.OpenAL")]
|
||||
[assembly: InternalsVisibleTo("ANX.SoundSystem.OpenAL")]
|
||||
[assembly: InternalsVisibleTo("ANX.Tools.XNBInspector")]
|
||||
[assembly: InternalsVisibleTo("ANX.Framework.Content.Pipeline")]
|
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using ANX.Framework.NonXNA;
|
||||
using ANX.Framework.NonXNA.Development;
|
||||
using ANX.Framework.NonXNA.PlatformSystem;
|
||||
|
||||
// This file is part of the ANX.Framework created by the
|
||||
@ -9,38 +9,27 @@ using ANX.Framework.NonXNA.PlatformSystem;
|
||||
|
||||
namespace ANX.Framework.Storage
|
||||
{
|
||||
[Developer("AstrorEnales")]
|
||||
[TestState(TestStateAttribute.TestState.Untested)]
|
||||
public class StorageContainer : IDisposable
|
||||
{
|
||||
private INativeStorageContainer nativeImplementation;
|
||||
private readonly INativeStorageContainer nativeImplementation;
|
||||
|
||||
#region Public
|
||||
public event EventHandler<EventArgs> Disposing;
|
||||
|
||||
public string DisplayName
|
||||
{
|
||||
get;
|
||||
protected set;
|
||||
}
|
||||
public string DisplayName { get; protected set; }
|
||||
public StorageDevice StorageDevice { get; protected set; }
|
||||
public bool IsDisposed { get; protected set; }
|
||||
#endregion
|
||||
|
||||
public StorageDevice StorageDevice
|
||||
{
|
||||
get;
|
||||
protected set;
|
||||
}
|
||||
|
||||
public bool IsDisposed
|
||||
{
|
||||
get;
|
||||
protected set;
|
||||
}
|
||||
#endregion
|
||||
|
||||
internal StorageContainer(StorageDevice device, PlayerIndex player,
|
||||
string displayName)
|
||||
internal StorageContainer(StorageDevice device, PlayerIndex player, string displayName)
|
||||
{
|
||||
StorageDevice = device;
|
||||
DisplayName = displayName;
|
||||
|
||||
// TODO: player!
|
||||
|
||||
nativeImplementation = PlatformSystem.Instance.CreateStorageContainer(this);
|
||||
}
|
||||
|
||||
@ -117,7 +106,8 @@ namespace ANX.Framework.Storage
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disposing(this, EventArgs.Empty);
|
||||
if (Disposing != null)
|
||||
Disposing(this, EventArgs.Empty);
|
||||
IsDisposed = true;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using ANX.Framework.NonXNA;
|
||||
using ANX.Framework.NonXNA.Development;
|
||||
using ANX.Framework.NonXNA.PlatformSystem;
|
||||
|
||||
// This file is part of the ANX.Framework created by the
|
||||
@ -9,6 +9,8 @@ using ANX.Framework.NonXNA.PlatformSystem;
|
||||
|
||||
namespace ANX.Framework.Storage
|
||||
{
|
||||
[Developer("AstrorEnales")]
|
||||
[TestState(TestStateAttribute.TestState.Untested)]
|
||||
public sealed class StorageDevice
|
||||
{
|
||||
#region Private
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Serialization;
|
||||
using ANX.Framework.NonXNA.Development;
|
||||
|
||||
#endregion // Using Statements
|
||||
|
||||
@ -11,6 +12,8 @@ using System.Runtime.Serialization;
|
||||
|
||||
namespace ANX.Framework.Storage
|
||||
{
|
||||
[Developer("AstrorEnales")]
|
||||
[TestState(TestStateAttribute.TestState.Untested)]
|
||||
public class StorageDeviceNotConnectedException : ExternalException
|
||||
{
|
||||
public StorageDeviceNotConnectedException()
|
||||
|
@ -42,6 +42,7 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="Creator.cs" />
|
||||
<Compile Include="OpenALAudioListener.cs" />
|
||||
<Compile Include="OpenALDynamicSoundEffectInstance.cs" />
|
||||
<Compile Include="OpenALSong.cs" />
|
||||
<Compile Include="OpenALSoundEffect.cs" />
|
||||
<Compile Include="OpenALSoundEffectInstance.cs" />
|
||||
|
@ -42,6 +42,7 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="Creator.cs" />
|
||||
<Compile Include="OpenALAudioListener.cs" />
|
||||
<Compile Include="OpenALDynamicSoundEffectInstance.cs" />
|
||||
<Compile Include="OpenALSong.cs" />
|
||||
<Compile Include="OpenALSoundEffect.cs" />
|
||||
<Compile Include="OpenALSoundEffectInstance.cs" />
|
||||
|
@ -43,6 +43,7 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="Creator.cs" />
|
||||
<Compile Include="OpenALAudioListener.cs" />
|
||||
<Compile Include="OpenALDynamicSoundEffectInstance.cs" />
|
||||
<Compile Include="OpenALSong.cs" />
|
||||
<Compile Include="OpenALSoundEffect.cs" />
|
||||
<Compile Include="OpenALSoundEffectInstance.cs" />
|
||||
|
@ -44,6 +44,7 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="Creator.cs" />
|
||||
<Compile Include="OpenALAudioListener.cs" />
|
||||
<Compile Include="OpenALDynamicSoundEffectInstance.cs" />
|
||||
<Compile Include="OpenALSong.cs" />
|
||||
<Compile Include="OpenALSoundEffect.cs" />
|
||||
<Compile Include="OpenALSoundEffectInstance.cs" />
|
||||
|
@ -115,7 +115,15 @@ namespace ANX.SoundSystem.OpenAL
|
||||
PreventSystemChange();
|
||||
return new OpenALSoundEffect(buffer, offset, count, sampleRate, channels, loopStart, loopLength);
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region CreateDynamicSoundEffectInstance
|
||||
public IDynamicSoundEffectInstance CreateDynamicSoundEffectInstance(int sampleRate, AudioChannels channels)
|
||||
{
|
||||
PreventSystemChange();
|
||||
return new OpenALDynamicSoundEffectInstance(sampleRate, channels);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CreateAudioListener
|
||||
public IAudioListener CreateAudioListener()
|
||||
@ -133,7 +141,7 @@ namespace ANX.SoundSystem.OpenAL
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CreateMicrophone
|
||||
#region CreateMicrophone (TODO)
|
||||
public IMicrophone CreateMicrophone(Microphone managedMicrophone)
|
||||
{
|
||||
PreventSystemChange();
|
||||
@ -141,22 +149,23 @@ namespace ANX.SoundSystem.OpenAL
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region GetAllMicrophones
|
||||
public ReadOnlyCollection<Microphone> GetAllMicrophones()
|
||||
#region GetAllMicrophones (TODO)
|
||||
public ReadOnlyCollection<Microphone> GetAllMicrophones()
|
||||
{
|
||||
PreventSystemChange();
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region GetDefaultMicrophone
|
||||
public int GetDefaultMicrophone(ReadOnlyCollection<Microphone> allMicrophones)
|
||||
#region GetDefaultMicrophone (TODO)
|
||||
public int GetDefaultMicrophone(ReadOnlyCollection<Microphone> allMicrophones)
|
||||
{
|
||||
PreventSystemChange();
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CreateSong
|
||||
public ISong CreateSong(Song parentSong, Uri uri)
|
||||
{
|
||||
PreventSystemChange();
|
||||
@ -168,12 +177,7 @@ namespace ANX.SoundSystem.OpenAL
|
||||
PreventSystemChange();
|
||||
return new OpenALSong(parentSong, filepath, duration);
|
||||
}
|
||||
|
||||
public IDynamicSoundEffectInstance CreateDynamicSoundEffectInstance()
|
||||
{
|
||||
PreventSystemChange();
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
#endregion
|
||||
|
||||
private static void PreventSystemChange()
|
||||
{
|
||||
|
@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using ANX.Framework;
|
||||
using ANX.Framework.Audio;
|
||||
using ANX.Framework.NonXNA.Development;
|
||||
using ANX.Framework.NonXNA.SoundSystem;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
|
||||
// This file is part of the ANX.Framework created by the
|
||||
// "ANX.Framework developer group" and released under the Ms-PL license.
|
||||
// For details see: http://anxframework.codeplex.com/license
|
||||
|
||||
namespace ANX.SoundSystem.OpenAL
|
||||
{
|
||||
[Developer("AstrorEnales")]
|
||||
public class OpenALDynamicSoundEffectInstance : IDynamicSoundEffectInstance
|
||||
{
|
||||
private int source;
|
||||
private readonly int sampleRate;
|
||||
private readonly int channels;
|
||||
private readonly ALFormat format;
|
||||
|
||||
public event EventHandler<EventArgs> BufferNeeded;
|
||||
|
||||
public int PendingBufferCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int queueSize;
|
||||
AL.GetSource(source, ALGetSourcei.BuffersQueued, out queueSize);
|
||||
return queueSize;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLooped
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
set { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
public float Pan
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
set { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
public float Pitch
|
||||
{
|
||||
get
|
||||
{
|
||||
float value;
|
||||
AL.GetSource(source, ALSourcef.Pitch, out value);
|
||||
return value;
|
||||
}
|
||||
set { AL.Source(source, ALSourcef.Pitch, value); }
|
||||
}
|
||||
|
||||
public SoundState State { get; private set; }
|
||||
|
||||
public float Volume
|
||||
{
|
||||
get
|
||||
{
|
||||
float value;
|
||||
AL.GetSource(source, ALSourcef.Gain, out value);
|
||||
return value;
|
||||
}
|
||||
set { AL.Source(source, ALSourcef.Gain, value); }
|
||||
}
|
||||
|
||||
public OpenALDynamicSoundEffectInstance(int setSampleRate, AudioChannels setChannels)
|
||||
{
|
||||
State = SoundState.Stopped;
|
||||
sampleRate = setSampleRate;
|
||||
channels = (int)setChannels;
|
||||
format = channels == 1 ? ALFormat.Mono16 : ALFormat.Stereo16;
|
||||
source = AL.GenSource();
|
||||
|
||||
FrameworkDispatcher.OnUpdate += Tick;
|
||||
}
|
||||
|
||||
public void SubmitBuffer(byte[] buffer)
|
||||
{
|
||||
SubmitBuffer(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
public unsafe void SubmitBuffer(byte[] buffer, int offset, int count)
|
||||
{
|
||||
int bufferHandle = AL.GenBuffer();
|
||||
IntPtr dataHandle;
|
||||
fixed (byte* ptr = &buffer[0])
|
||||
dataHandle = (IntPtr)(ptr + offset);
|
||||
AL.BufferData(bufferHandle, format, dataHandle, count, sampleRate);
|
||||
AL.SourceQueueBuffer(source, bufferHandle);
|
||||
}
|
||||
|
||||
public void Play()
|
||||
{
|
||||
if (State != SoundState.Playing)
|
||||
AL.SourcePlay(source);
|
||||
}
|
||||
|
||||
public void Pause()
|
||||
{
|
||||
if (State != SoundState.Paused)
|
||||
AL.SourcePause(source);
|
||||
}
|
||||
|
||||
public void Stop(bool immediate)
|
||||
{
|
||||
// TODO: handle immediate!
|
||||
|
||||
if (State != SoundState.Stopped)
|
||||
AL.SourceStop(source);
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
Play();
|
||||
}
|
||||
|
||||
private void Tick()
|
||||
{
|
||||
if (State != SoundState.Playing)
|
||||
return;
|
||||
|
||||
int buffersProcessed;
|
||||
AL.GetSource(source, ALGetSourcei.BuffersProcessed, out buffersProcessed);
|
||||
for (int index = 0; index < buffersProcessed; index++)
|
||||
{
|
||||
int buffer = AL.SourceUnqueueBuffer(source);
|
||||
AL.DeleteBuffer(buffer);
|
||||
}
|
||||
|
||||
if (PendingBufferCount == 0 && BufferNeeded != null)
|
||||
BufferNeeded(null, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void Apply3D(AudioListener[] listeners, AudioEmitter emitter)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (source != -1)
|
||||
{
|
||||
FrameworkDispatcher.OnUpdate -= Tick;
|
||||
AL.DeleteSource(source);
|
||||
}
|
||||
|
||||
source = -1;
|
||||
}
|
||||
}
|
||||
}
|
@ -103,8 +103,7 @@ namespace ANX.SoundSystem.PsVita
|
||||
#endregion
|
||||
|
||||
#region CreateSoundEffectInstance
|
||||
public ISoundEffectInstance CreateSoundEffectInstance(
|
||||
ISoundEffect nativeSoundEffect)
|
||||
public ISoundEffectInstance CreateSoundEffectInstance(ISoundEffect nativeSoundEffect)
|
||||
{
|
||||
AddInSystemFactory.Instance.PreventSystemChange(AddInType.SoundSystem);
|
||||
throw new NotImplementedException();
|
||||
@ -178,10 +177,10 @@ namespace ANX.SoundSystem.PsVita
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IDynamicSoundEffectInstance CreateDynamicSoundEffectInstance()
|
||||
public IDynamicSoundEffectInstance CreateDynamicSoundEffectInstance(int sampleRate, AudioChannels channels)
|
||||
{
|
||||
AddInSystemFactory.Instance.PreventSystemChange(AddInType.SoundSystem);
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Creator.cs" />
|
||||
<Compile Include="XAudioDynamicSoundEffectInstance.cs" />
|
||||
<Compile Include="XAudioOggInputStream.cs" />
|
||||
<Compile Include="XAudioSong.cs" />
|
||||
<Compile Include="XAudioSoundEffectInstance.cs" />
|
||||
|
@ -44,6 +44,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Creator.cs" />
|
||||
<Compile Include="XAudioDynamicSoundEffectInstance.cs" />
|
||||
<Compile Include="XAudioOggInputStream.cs" />
|
||||
<Compile Include="XAudioSong.cs" />
|
||||
<Compile Include="XAudioSoundEffectInstance.cs" />
|
||||
|
@ -45,6 +45,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Creator.cs" />
|
||||
<Compile Include="XAudioDynamicSoundEffectInstance.cs" />
|
||||
<Compile Include="XAudioOggInputStream.cs" />
|
||||
<Compile Include="XAudioSong.cs" />
|
||||
<Compile Include="XAudioSoundEffectInstance.cs" />
|
||||
|
@ -45,6 +45,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Creator.cs" />
|
||||
<Compile Include="XAudioDynamicSoundEffectInstance.cs" />
|
||||
<Compile Include="XAudioOggInputStream.cs" />
|
||||
<Compile Include="XAudioSong.cs" />
|
||||
<Compile Include="XAudioSoundEffectInstance.cs" />
|
||||
|
@ -148,13 +148,15 @@ namespace ANX.SoundSystem.Windows.XAudio
|
||||
PreventSystemChange();
|
||||
return new XAudioSoundEffectInstance(device, nativeSoundEffect as XAudioSoundEffect);
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
public IDynamicSoundEffectInstance CreateDynamicSoundEffectInstance()
|
||||
#region CreateDynamicSoundEffectInstance
|
||||
public IDynamicSoundEffectInstance CreateDynamicSoundEffectInstance(int sampleRate, AudioChannels channels)
|
||||
{
|
||||
PreventSystemChange();
|
||||
throw new NotImplementedException();
|
||||
return new XAudioDynamicSoundEffectInstance(device, sampleRate, channels);
|
||||
}
|
||||
#endregion
|
||||
|
||||
public IMicrophone CreateMicrophone(Microphone managedMicrophone)
|
||||
{
|
||||
|
@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using ANX.Framework.Audio;
|
||||
using ANX.Framework.NonXNA.Development;
|
||||
using ANX.Framework.NonXNA.SoundSystem;
|
||||
using SharpDX;
|
||||
using SharpDX.Multimedia;
|
||||
using SharpDX.XAudio2;
|
||||
|
||||
// This file is part of the ANX.Framework created by the
|
||||
// "ANX.Framework developer group" and released under the Ms-PL license.
|
||||
// For details see: http://anxframework.codeplex.com/license
|
||||
|
||||
namespace ANX.SoundSystem.Windows.XAudio
|
||||
{
|
||||
/// <summary>
|
||||
/// DynamicSoundEffectInstance supports only PCM 16-bit mono or stereo data.
|
||||
/// </summary>
|
||||
[Developer("AstrorEnales")]
|
||||
public class XAudioDynamicSoundEffectInstance : IDynamicSoundEffectInstance
|
||||
{
|
||||
private SourceVoice source;
|
||||
private float currentPitch;
|
||||
|
||||
public event EventHandler<EventArgs> BufferNeeded;
|
||||
|
||||
public int PendingBufferCount
|
||||
{
|
||||
get { return source.State.BuffersQueued; }
|
||||
}
|
||||
|
||||
public bool IsLooped
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
set { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
public float Pan
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
set { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
public float Pitch
|
||||
{
|
||||
get { return currentPitch; }
|
||||
set
|
||||
{
|
||||
currentPitch = value;
|
||||
source.SetFrequencyRatio(value);
|
||||
// TODO: pitch <= 1 is working, but greater isn't
|
||||
if (value > 1f)
|
||||
throw new NotImplementedException("Pitch greater than 1f is currently not implemented for XAudio!");
|
||||
}
|
||||
}
|
||||
|
||||
public SoundState State { get; private set; }
|
||||
|
||||
public float Volume
|
||||
{
|
||||
get { return source.Volume; }
|
||||
set { source.SetVolume(value, 0); }
|
||||
}
|
||||
|
||||
public XAudioDynamicSoundEffectInstance(XAudio2 device, int sampleRate, AudioChannels channels)
|
||||
{
|
||||
State = SoundState.Stopped;
|
||||
currentPitch = 1f;
|
||||
var format = new WaveFormat(sampleRate, 16, (int)channels);
|
||||
source = new SourceVoice(device, format, true);
|
||||
source.BufferEnd += OnBufferEnd;
|
||||
}
|
||||
|
||||
private void OnBufferEnd(IntPtr handle)
|
||||
{
|
||||
if (source.State.BuffersQueued == 0 && BufferNeeded != null)
|
||||
BufferNeeded(null, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void SubmitBuffer(byte[] buffer)
|
||||
{
|
||||
SubmitBuffer(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
public unsafe void SubmitBuffer(byte[] buffer, int offset, int count)
|
||||
{
|
||||
var audioBuffer = new AudioBuffer();
|
||||
IntPtr dataHandle;
|
||||
fixed (byte* ptr = &buffer[0])
|
||||
dataHandle = (IntPtr)(ptr + offset);
|
||||
audioBuffer.Stream = new DataStream(dataHandle, count, false, false);
|
||||
source.SubmitSourceBuffer(audioBuffer, null);
|
||||
}
|
||||
|
||||
public void Play()
|
||||
{
|
||||
if (State != SoundState.Playing)
|
||||
source.Start();
|
||||
}
|
||||
|
||||
public void Pause()
|
||||
{
|
||||
if (State != SoundState.Paused)
|
||||
source.Stop();
|
||||
}
|
||||
|
||||
public void Stop(bool immediate)
|
||||
{
|
||||
// TODO: handle immediate
|
||||
|
||||
if (State != SoundState.Stopped)
|
||||
source.Stop();
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
Play();
|
||||
}
|
||||
|
||||
public void Apply3D(AudioListener[] listeners, AudioEmitter emitter)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (source != null)
|
||||
{
|
||||
source.BufferEnd -= OnBufferEnd;
|
||||
source.DestroyVoice();
|
||||
source.Dispose();
|
||||
}
|
||||
|
||||
source = null;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user