2018-03-16 01:06:24 +01:00
using OpenTK.Audio ;
using OpenTK.Audio.OpenAL ;
using System ;
using System.Collections.Concurrent ;
2018-07-15 04:57:41 +02:00
using System.Runtime.InteropServices ;
2018-03-19 19:58:46 +01:00
using System.Threading ;
2018-03-16 01:06:24 +01:00
2018-11-15 03:22:50 +01:00
namespace Ryujinx.Audio
2018-03-16 01:06:24 +01:00
{
2018-11-15 03:22:50 +01:00
/// <summary>
/// An audio renderer that uses OpenAL as the audio backend
/// </summary>
2018-08-17 01:47:36 +02:00
public class OpenALAudioOut : IAalOutput , IDisposable
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
/// <summary>
/// The maximum amount of tracks we can issue simultaneously
/// </summary>
2018-03-16 01:06:24 +01:00
private const int MaxTracks = 256 ;
2019-10-11 17:54:29 +02:00
/// <summary>
/// The <see cref="OpenTK.Audio"/> audio context
/// </summary>
private AudioContext _context ;
2018-03-19 19:58:46 +01:00
2019-10-11 17:54:29 +02:00
/// <summary>
/// An object pool containing <see cref="OpenALAudioTrack"/> objects
/// </summary>
private ConcurrentDictionary < int , OpenALAudioTrack > _tracks ;
2018-03-16 01:06:24 +01:00
2019-10-11 17:54:29 +02:00
/// <summary>
/// True if the thread need to keep polling
/// </summary>
private bool _keepPolling ;
2018-03-19 19:58:46 +01:00
2019-10-11 17:54:29 +02:00
/// <summary>
/// The poller thread audio context
/// </summary>
private Thread _audioPollerThread ;
2018-03-19 19:58:46 +01:00
2018-11-15 03:22:50 +01:00
/// <summary>
2019-10-11 17:54:29 +02:00
/// True if OpenAL is supported on the device
2018-11-15 03:22:50 +01:00
/// </summary>
public static bool IsSupported
{
get
{
try
{
return AudioContext . AvailableDevices . Count > 0 ;
}
catch
{
return false ;
}
}
}
2019-10-11 17:54:29 +02:00
public OpenALAudioOut ( )
{
_context = new AudioContext ( ) ;
_tracks = new ConcurrentDictionary < int , OpenALAudioTrack > ( ) ;
_keepPolling = true ;
2020-01-13 01:21:54 +01:00
_audioPollerThread = new Thread ( AudioPollerWork )
{
Name = "Audio.PollerThread"
} ;
2019-10-11 17:54:29 +02:00
_audioPollerThread . Start ( ) ;
}
2018-03-19 19:58:46 +01:00
private void AudioPollerWork ( )
{
do
{
2019-10-11 17:54:29 +02:00
foreach ( OpenALAudioTrack track in _tracks . Values )
2018-03-19 19:58:46 +01:00
{
2019-10-11 17:54:29 +02:00
lock ( track )
2018-10-12 23:47:53 +02:00
{
2019-10-11 17:54:29 +02:00
track . CallReleaseCallbackIfNeeded ( ) ;
2018-10-12 23:47:53 +02:00
}
2018-03-19 19:58:46 +01:00
}
2019-07-02 04:39:22 +02:00
// If it's not slept it will waste cycles.
2018-08-01 05:48:49 +02:00
Thread . Sleep ( 10 ) ;
2018-03-19 19:58:46 +01:00
}
2019-10-11 17:54:29 +02:00
while ( _keepPolling ) ;
2018-08-17 01:47:36 +02:00
2019-10-11 17:54:29 +02:00
foreach ( OpenALAudioTrack track in _tracks . Values )
2018-08-17 01:47:36 +02:00
{
2019-10-11 17:54:29 +02:00
track . Dispose ( ) ;
2018-08-17 01:47:36 +02:00
}
2019-10-11 17:54:29 +02:00
_tracks . Clear ( ) ;
2020-02-06 12:38:24 +01:00
_context . Dispose ( ) ;
2018-03-16 01:06:24 +01:00
}
2020-08-18 21:03:55 +02:00
public bool SupportsChannelCount ( int channels )
{
// NOTE: OpenAL doesn't give us a way to know if the 5.1 setup is supported by hardware or actually emulated.
// TODO: find a way to determine hardware support.
return channels = = 1 | | channels = = 2 ;
}
2019-10-11 17:54:29 +02:00
/// <summary>
/// Creates a new audio track with the specified parameters
/// </summary>
/// <param name="sampleRate">The requested sample rate</param>
2020-08-18 21:03:55 +02:00
/// <param name="hardwareChannels">The requested hardware channels</param>
/// <param name="virtualChannels">The requested virtual channels</param>
2019-10-11 17:54:29 +02:00
/// <param name="callback">A <see cref="ReleaseCallback" /> that represents the delegate to invoke when a buffer has been released by the audio track</param>
2020-08-18 21:03:55 +02:00
/// <returns>The created track's Track ID</returns>
public int OpenHardwareTrack ( int sampleRate , int hardwareChannels , int virtualChannels , ReleaseCallback callback )
2018-03-16 01:06:24 +01:00
{
2020-08-18 21:03:55 +02:00
OpenALAudioTrack track = new OpenALAudioTrack ( sampleRate , GetALFormat ( hardwareChannels ) , hardwareChannels , virtualChannels , callback ) ;
2018-03-16 01:06:24 +01:00
2019-10-11 17:54:29 +02:00
for ( int id = 0 ; id < MaxTracks ; id + + )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
if ( _tracks . TryAdd ( id , track ) )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
return id ;
2018-03-16 01:06:24 +01:00
}
}
return - 1 ;
}
2019-10-11 17:54:29 +02:00
private ALFormat GetALFormat ( int channels )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
switch ( channels )
2018-07-10 03:49:07 +02:00
{
2018-07-15 04:57:41 +02:00
case 1 : return ALFormat . Mono16 ;
case 2 : return ALFormat . Stereo16 ;
case 6 : return ALFormat . Multi51Chn16Ext ;
2018-07-10 03:49:07 +02:00
}
2018-03-16 01:06:24 +01:00
2019-10-11 17:54:29 +02:00
throw new ArgumentOutOfRangeException ( nameof ( channels ) ) ;
2018-03-16 01:06:24 +01:00
}
2019-10-11 17:54:29 +02:00
/// <summary>
/// Stops playback and closes the track specified by <paramref name="trackId"/>
/// </summary>
/// <param name="trackId">The ID of the track to close</param>
public void CloseTrack ( int trackId )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
if ( _tracks . TryRemove ( trackId , out OpenALAudioTrack track ) )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
lock ( track )
2018-09-18 05:12:47 +02:00
{
2019-10-11 17:54:29 +02:00
track . Dispose ( ) ;
2018-09-18 05:12:47 +02:00
}
2018-03-16 01:06:24 +01:00
}
}
2019-10-11 17:54:29 +02:00
/// <summary>
/// Returns a value indicating whether the specified buffer is currently reserved by the specified track
/// </summary>
/// <param name="trackId">The track to check</param>
/// <param name="bufferTag">The buffer tag to check</param>
public bool ContainsBuffer ( int trackId , long bufferTag )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
if ( _tracks . TryGetValue ( trackId , out OpenALAudioTrack track ) )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
lock ( track )
2018-09-18 05:12:47 +02:00
{
2019-10-11 17:54:29 +02:00
return track . ContainsBuffer ( bufferTag ) ;
2018-09-18 05:12:47 +02:00
}
2018-03-16 01:06:24 +01:00
}
2018-07-10 03:49:07 +02:00
2018-03-16 04:42:44 +01:00
return false ;
2018-03-16 01:06:24 +01:00
}
2019-10-11 17:54:29 +02:00
/// <summary>
/// Gets a list of buffer tags the specified track is no longer reserving
/// </summary>
/// <param name="trackId">The track to retrieve buffer tags from</param>
/// <param name="maxCount">The maximum amount of buffer tags to retrieve</param>
/// <returns>Buffers released by the specified track</returns>
public long [ ] GetReleasedBuffers ( int trackId , int maxCount )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
if ( _tracks . TryGetValue ( trackId , out OpenALAudioTrack track ) )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
lock ( track )
2018-09-18 05:12:47 +02:00
{
2019-10-11 17:54:29 +02:00
return track . GetReleasedBuffers ( maxCount ) ;
2018-09-18 05:12:47 +02:00
}
2018-03-16 01:06:24 +01:00
}
2018-07-10 03:49:07 +02:00
2018-03-16 04:42:44 +01:00
return null ;
2018-03-16 01:06:24 +01:00
}
2019-10-11 17:54:29 +02:00
/// <summary>
/// Appends an audio buffer to the specified track
/// </summary>
/// <typeparam name="T">The sample type of the buffer</typeparam>
/// <param name="trackId">The track to append the buffer to</param>
/// <param name="bufferTag">The internal tag of the buffer</param>
/// <param name="buffer">The buffer to append to the track</param>
public void AppendBuffer < T > ( int trackId , long bufferTag , T [ ] buffer ) where T : struct
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
if ( _tracks . TryGetValue ( trackId , out OpenALAudioTrack track ) )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
lock ( track )
2018-09-18 05:12:47 +02:00
{
2019-10-11 17:54:29 +02:00
int bufferId = track . AppendBuffer ( bufferTag ) ;
2018-03-16 04:42:44 +01:00
2020-08-18 21:03:55 +02:00
// Do we need to downmix?
if ( track . HardwareChannels ! = track . VirtualChannels )
{
short [ ] downmixedBuffer ;
ReadOnlySpan < short > bufferPCM16 = MemoryMarshal . Cast < T , short > ( buffer ) ;
if ( track . VirtualChannels = = 6 )
{
downmixedBuffer = Downmixing . DownMixSurroundToStereo ( bufferPCM16 ) ;
if ( track . HardwareChannels = = 1 )
{
downmixedBuffer = Downmixing . DownMixStereoToMono ( downmixedBuffer ) ;
}
}
else if ( track . VirtualChannels = = 2 )
{
downmixedBuffer = Downmixing . DownMixStereoToMono ( bufferPCM16 ) ;
}
else
{
throw new NotImplementedException ( $"Downmixing from {track.VirtualChannels} to {track.HardwareChannels} not implemented!" ) ;
}
AL . BufferData ( bufferId , track . Format , downmixedBuffer , downmixedBuffer . Length * sizeof ( ushort ) , track . SampleRate ) ;
}
else
{
AL . BufferData ( bufferId , track . Format , buffer , buffer . Length * sizeof ( ushort ) , track . SampleRate ) ;
}
2018-03-16 04:42:44 +01:00
2019-10-11 17:54:29 +02:00
AL . SourceQueueBuffer ( track . SourceId , bufferId ) ;
2018-03-16 04:42:44 +01:00
2019-10-11 17:54:29 +02:00
StartPlaybackIfNeeded ( track ) ;
2020-11-20 21:59:01 +01:00
track . PlayedSampleCount + = ( ulong ) buffer . Length ;
2018-09-18 05:12:47 +02:00
}
2018-03-16 01:06:24 +01:00
}
}
2019-10-11 17:54:29 +02:00
/// <summary>
/// Starts playback
/// </summary>
/// <param name="trackId">The ID of the track to start playback on</param>
public void Start ( int trackId )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
if ( _tracks . TryGetValue ( trackId , out OpenALAudioTrack track ) )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
lock ( track )
2018-09-18 05:12:47 +02:00
{
2019-10-11 17:54:29 +02:00
track . State = PlaybackState . Playing ;
2018-03-16 01:06:24 +01:00
2019-10-11 17:54:29 +02:00
StartPlaybackIfNeeded ( track ) ;
2018-09-18 05:12:47 +02:00
}
2018-03-16 01:06:24 +01:00
}
}
2019-10-11 17:54:29 +02:00
private void StartPlaybackIfNeeded ( OpenALAudioTrack track )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
AL . GetSource ( track . SourceId , ALGetSourcei . SourceState , out int stateInt ) ;
2018-03-16 01:06:24 +01:00
2019-10-11 17:54:29 +02:00
ALSourceState State = ( ALSourceState ) stateInt ;
2018-03-16 01:06:24 +01:00
2019-10-11 17:54:29 +02:00
if ( State ! = ALSourceState . Playing & & track . State = = PlaybackState . Playing )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
AL . SourcePlay ( track . SourceId ) ;
2018-03-16 01:06:24 +01:00
}
}
2019-10-11 17:54:29 +02:00
/// <summary>
/// Stops playback
/// </summary>
/// <param name="trackId">The ID of the track to stop playback on</param>
public void Stop ( int trackId )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
if ( _tracks . TryGetValue ( trackId , out OpenALAudioTrack track ) )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
lock ( track )
2018-09-18 05:12:47 +02:00
{
2019-10-11 17:54:29 +02:00
track . State = PlaybackState . Stopped ;
2018-03-16 01:06:24 +01:00
2019-10-11 17:54:29 +02:00
AL . SourceStop ( track . SourceId ) ;
2018-09-18 05:12:47 +02:00
}
2018-03-16 01:06:24 +01:00
}
}
2019-10-11 17:54:29 +02:00
/// <summary>
2020-11-20 21:59:01 +01:00
/// Get track buffer count
2019-10-11 17:54:29 +02:00
/// </summary>
2020-11-20 21:59:01 +01:00
/// <param name="trackId">The ID of the track to get buffer count</param>
public uint GetBufferCount ( int trackId )
{
if ( _tracks . TryGetValue ( trackId , out OpenALAudioTrack track ) )
{
lock ( track )
{
return track . BufferCount ;
}
}
return 0 ;
}
2019-10-11 17:54:29 +02:00
/// <summary>
2020-11-20 21:59:01 +01:00
/// Get track played sample count
2019-10-11 17:54:29 +02:00
/// </summary>
2020-11-20 21:59:01 +01:00
/// <param name="trackId">The ID of the track to get played sample count</param>
public ulong GetPlayedSampleCount ( int trackId )
2019-10-11 17:54:29 +02:00
{
2020-11-20 21:59:01 +01:00
if ( _tracks . TryGetValue ( trackId , out OpenALAudioTrack track ) )
2019-10-11 17:54:29 +02:00
{
2020-11-20 21:59:01 +01:00
lock ( track )
{
return track . PlayedSampleCount ;
}
2019-10-11 17:54:29 +02:00
}
2020-11-20 21:59:01 +01:00
return 0 ;
}
/// <summary>
/// Flush all track buffers
/// </summary>
/// <param name="trackId">The ID of the track to flush</param>
public bool FlushBuffers ( int trackId )
{
if ( _tracks . TryGetValue ( trackId , out OpenALAudioTrack track ) )
{
lock ( track )
{
track . FlushBuffers ( ) ;
}
}
return false ;
}
/// <summary>
/// Set track volume
/// </summary>
/// <param name="trackId">The ID of the track to set volume</param>
/// <param name="volume">The volume of the track</param>
public void SetVolume ( int trackId , float volume )
{
if ( _tracks . TryGetValue ( trackId , out OpenALAudioTrack track ) )
{
lock ( track )
{
track . SetVolume ( volume ) ;
}
}
}
/// <summary>
/// Get track volume
/// </summary>
/// <param name="trackId">The ID of the track to get volume</param>
public float GetVolume ( int trackId )
{
if ( _tracks . TryGetValue ( trackId , out OpenALAudioTrack track ) )
{
lock ( track )
{
2020-11-27 20:55:00 +01:00
return track . GetVolume ( ) ;
2020-11-20 21:59:01 +01:00
}
}
return 1.0f ;
2019-10-11 17:54:29 +02:00
}
/// <summary>
/// Gets the current playback state of the specified track
/// </summary>
/// <param name="trackId">The track to retrieve the playback state for</param>
public PlaybackState GetState ( int trackId )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
if ( _tracks . TryGetValue ( trackId , out OpenALAudioTrack track ) )
2018-03-16 01:06:24 +01:00
{
2019-10-11 17:54:29 +02:00
return track . State ;
2018-03-16 01:06:24 +01:00
}
return PlaybackState . Stopped ;
}
2018-08-17 01:47:36 +02:00
public void Dispose ( )
{
Dispose ( true ) ;
}
2019-10-11 17:54:29 +02:00
protected virtual void Dispose ( bool disposing )
2018-08-17 01:47:36 +02:00
{
2019-10-11 17:54:29 +02:00
if ( disposing )
2018-08-17 01:47:36 +02:00
{
2019-10-11 17:54:29 +02:00
_keepPolling = false ;
2018-08-17 01:47:36 +02:00
}
}
2018-03-16 01:06:24 +01:00
}
}