diff --git a/.gitignore b/.gitignore index add57be..ecdece2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,376 @@ -bin/ -obj/ -/packages/ -riderModule.iml -/_ReSharper.Caches/ \ No newline at end of file +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Visual Studio Сode options directory +.vscode/ +# JetBrains Rider cache/options directory +.idea/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*[.json, .xml, .info] + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# Executing programs +*.exe + +# NuGet +NuGet.config + +# DocFX site +_site/ + +# JSON Schemas +JSON/ \ No newline at end of file diff --git a/README.md b/README.md index 3977dc5..5d163c9 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ This is an API Library that does not depend on any Plugin frameworks for its function. This API provides a implementation of an Audio player in a SCP:SL Server with options to extend +That API uses EXILED: [Exiled-Team/EXILED](https://github.com/Exiled-Team/EXILED) + ## Installation Download the latest release from the [Releases](https://github.com/CedModV2/SCPSLAudioApi/releases/) page and place the SCPSLAudioAPi.dll file in your plugin dependencies folder diff --git a/SCPSLAudioApi/App.config b/SCPSLAudioApi/App.config index 7c07b68..11a7fe9 100644 --- a/SCPSLAudioApi/App.config +++ b/SCPSLAudioApi/App.config @@ -1,10 +1,15 @@ - - - - - - - - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SCPSLAudioApi/AudioCore/AudioPlayerBase.cs b/SCPSLAudioApi/AudioCore/AudioPlayerBase.cs index b0a13c9..4597464 100644 --- a/SCPSLAudioApi/AudioCore/AudioPlayerBase.cs +++ b/SCPSLAudioApi/AudioCore/AudioPlayerBase.cs @@ -2,13 +2,15 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Exiled.API.Features; using MEC; using NVorbis; -using PluginAPI.Core; +using SCPSLAudioApi.Events.Handlers; using UnityEngine; using UnityEngine.Networking; using VoiceChat; using VoiceChat.Codec; +using VoiceChat.Codec.Enums; using VoiceChat.Networking; using Random = UnityEngine.Random; @@ -16,151 +18,63 @@ namespace SCPSLAudioApi.AudioCore { public class AudioPlayerBase : MonoBehaviour { - public static Dictionary AudioPlayers = new Dictionary(); + public static Dictionary AudioPlayers; - #region Internal - - public const int HeadSamples = 1920; - public OpusEncoder Encoder { get; } = new OpusEncoder(VoiceChat.Codec.Enums.OpusApplicationType.Voip); - public PlaybackBuffer PlaybackBuffer { get; } = new PlaybackBuffer(); - public byte[] EncodedBuffer { get; } = new byte[512]; - public bool stopTrack = false; - public bool ready = false; - public CoroutineHandle PlaybackCoroutine; + public virtual void Update() + { + if (Owner == null || !ready || StreamBuffer.Count == 0 || !ShouldPlay) return; - public float allowedSamples; - public int samplesPerSecond; - public Queue StreamBuffer { get; } = new Queue(); - public VorbisReader VorbisReader { get; set; } - public float[] SendBuffer { get; set; } - public float[] ReadBuffer { get; set; } - - #endregion - - #region AudioPlayer Settings - - /// - /// The ReferenceHub instance that this player sends as. - /// - public ReferenceHub Owner { get; set; } - - /// - /// Volume that the player will play at. - /// - public float Volume { get; set; } = 100f; - - /// - /// List of Paths/Urls that the player will play from (Urls only work if is true) - /// - public List AudioToPlay = new List(); - - /// - /// Path/Url of the currently playing audio file. - /// - public string CurrentPlay; - - /// - /// Stream containing the Audio data - /// - public MemoryStream CurrentPlayStream; - - /// - /// Boolean indicating whether or not the Queue will loop (Audio will be added to the end of the queue after it gets removed on play) - /// - public bool Loop = false; - - /// - /// If the playlist should be shuffled when an audio track start. - /// - public bool Shuffle = false; - - /// - /// Whether the Player should continue playing by itself after the current Track ends. - /// - public bool Continue = true; - - /// - /// Whether the Player should be sending audio to the broadcaster. - /// - public bool ShouldPlay = true; - - /// - /// If URLs are allowed to be played - /// - public bool AllowUrl = false; - - /// - /// If Debug logs shouldbe shown (Note: can be very spammy) - /// - public bool LogDebug = false; + allowedSamples += Time.deltaTime * samplesPerSecond; + var toCopy = Mathf.Min(Mathf.FloorToInt(allowedSamples), StreamBuffer.Count); - /// - /// If not empty, the audio will only be sent to players with the PlayerIds in this list - /// - public List BroadcastTo = new List(); + if (VerboseLogs) + Log.Info($"1 {toCopy} {allowedSamples} {samplesPerSecond} " + + $"{StreamBuffer.Count} {PlaybackBuffer.Length} {PlaybackBuffer.WriteHead}"); - /// - /// Gets or Sets the Channel where audio will be played in - /// - public VoiceChatChannel BroadcastChannel { get; set; } = VoiceChatChannel.Proximity; + if (toCopy > 0) + for (var i = 0; i < toCopy; i++) + PlaybackBuffer.Write(StreamBuffer.Dequeue() * (Volume / 100f)); - #endregion + if (VerboseLogs) + Log.Info($"2 {toCopy} {allowedSamples} {samplesPerSecond} " + + $"{StreamBuffer.Count} {PlaybackBuffer.Length} {PlaybackBuffer.WriteHead}"); - #region Events - - /// - /// Fired when a track is getting selected. - /// - /// The AudioPlayer instance that this event fired for - /// If the AudioPlayer was playing Directly (-1 index) - /// Position in the Queue of the track that is going to be selected - public delegate void TrackSelecting(AudioPlayerBase playerBase, bool directPlay, ref int queuePos); - public static event TrackSelecting OnTrackSelecting; - - /// - /// Fired when a track has been selected - /// - /// The AudioPlayer instance that this event fired for - /// If the AudioPlayer was playing Directly (-1 index) - /// Position in the Queue of the track that will start - /// The track the AudioPlayer will play - public delegate void TrackSelected(AudioPlayerBase playerBase, bool directPlay, int queuePos, ref string track); - public static event TrackSelected OnTrackSelected; - - - /// - /// Fired when a track is loaded and will begin playing. - /// - /// The AudioPlayer instance that this event fired for - /// If the AudioPlayer was playing Directly (-1 index) - /// Position in the Queue that will play - /// The track the AudioPlayer will play - public delegate void TrackLoaded(AudioPlayerBase playerBase, bool directPlay, int queuePos, string track); - public static event TrackLoaded OnTrackLoaded; - - /// - /// Fired when a track finishes. - /// - /// The AudioPlayer instance that this event fired for - /// The track the AudioPlayer was playing - /// If the AudioPlayer was playing Directly (-1 index) - /// Position in the Queue that will play next, can be set to a different value - public delegate void TrackFinished(AudioPlayerBase playerBase, string track, bool directPlay, ref int nextQueuePos); - public static event TrackFinished OnFinishedTrack; + allowedSamples -= toCopy; - #endregion + while (PlaybackBuffer.Length >= 480) + { + PlaybackBuffer.ReadTo(SendBuffer, 480); + var dataLen = Encoder.Encode(SendBuffer, EncodedBuffer); + + foreach (var plr in ReferenceHub.AllHubs) + { + if (plr.connectionToClient == null || + (BroadcastTo.Count >= 1 && !BroadcastTo.Contains(plr.PlayerId))) continue; + + plr.connectionToClient.Send(new VoiceMessage(Owner, BroadcastChannel, + EncodedBuffer, dataLen, false)); + } + } + } + + public virtual void OnDestroy() + { + if (PlaybackCoroutine.IsValid) + Timing.KillCoroutines(PlaybackCoroutine); + + AudioPlayers.Remove(Owner); + } /// - /// Add or retrieve the AudioPlayerBase instance based on a ReferenceHub instance. + /// Add or retrieve the AudioPlayerBase instance based on a ReferenceHub instance. /// /// The ReferenceHub instance that this AudioPlayer belongs to - /// + /// + /// + /// public static AudioPlayerBase Get(ReferenceHub hub) { - if (AudioPlayers.TryGetValue(hub, out AudioPlayerBase player)) - { - return player; - } + if (AudioPlayers.TryGetValue(hub, out var player)) return player; player = hub.gameObject.AddComponent(); player.Owner = hub; @@ -170,31 +84,32 @@ public static AudioPlayerBase Get(ReferenceHub hub) } /// - /// Start playing audio, if called while audio is already playing the player will skip to the next file. + /// Start playing audio, if called while audio is already playing the player will skip to the next file. /// /// The position in the queue of the audio that should be played. public virtual void Play(int queuePos) { if (PlaybackCoroutine.IsRunning) Timing.KillCoroutines(PlaybackCoroutine); + PlaybackCoroutine = Timing.RunCoroutine(Playback(queuePos), Segment.FixedUpdate); } - + /// - /// Stops playing the current Track, or stops the player entirely if Clear is true. + /// Stops playing the current Track, or stops the player entirely if Clear is true. /// - /// If true the player will stop and the queue will be cleared. - public virtual void Stoptrack(bool Clear) + /// If true the player will stop and the queue will be cleared. + public virtual void Stoptrack(bool clear) { - if (Clear) - AudioToPlay.Clear(); + if (clear) AudioToPlay.Clear(); + stopTrack = true; } - + /// - /// Add an audio file to the queue + /// Add an audio file to the queue /// - /// Path/Url to an audio file (Url only works if is true) + /// Path/Url to an audio file (Url only works if is true) /// Position that the audio file should be inserted at, use -1 to insert at the end of the queue. public virtual void Enqueue(string audio, int pos) { @@ -204,52 +119,51 @@ public virtual void Enqueue(string audio, int pos) AudioToPlay.Insert(pos, audio); } - public virtual void OnDestroy() - { - if (PlaybackCoroutine.IsValid) - Timing.KillCoroutines(PlaybackCoroutine); - AudioPlayers.Remove(Owner); - } - public virtual IEnumerator Playback(int position) { stopTrack = false; - int index = position; - OnTrackSelecting?.Invoke(this, index == -1, ref index); + var index = position; + + Track.InvokeTrackSelectingEvent(this, index == -1, ref index); + if (index != -1) { if (Shuffle) AudioToPlay = AudioToPlay.OrderBy(i => Random.value).ToList(); + CurrentPlay = AudioToPlay[index]; AudioToPlay.RemoveAt(index); + if (Loop) - { AudioToPlay.Add(CurrentPlay); - } } - OnTrackSelected?.Invoke(this, index == -1, index, ref CurrentPlay); - Log.Info($"Loading Audio"); - if (AllowUrl && Uri.TryCreate(CurrentPlay, UriKind.Absolute, out Uri result)) + + Track.InvokeTrackSelectedEvent(this, index == -1, index, ref CurrentPlay); + Log.Debug("Loading Audio"); + + if (AllowUrl && Uri.TryCreate(CurrentPlay, UriKind.Absolute, out var result)) { - UnityWebRequest www = new UnityWebRequest(CurrentPlay, "GET"); - DownloadHandlerBuffer dH = new DownloadHandlerBuffer(); - www.downloadHandler = dH; - - yield return Timing.WaitUntilDone(www.SendWebRequest()); + var webRequest = new UnityWebRequest(CurrentPlay, "GET"); + var downloadHandler = new DownloadHandlerBuffer(); + webRequest.downloadHandler = downloadHandler; + + yield return Timing.WaitUntilDone(webRequest.SendWebRequest()); - if (www.responseCode != 200) + if (webRequest.responseCode != 200) { - Log.Error($"Failed to retrieve audio {www.responseCode} {www.downloadHandler.text}"); - if (Continue && AudioToPlay.Count >= 1) - { - yield return Timing.WaitForSeconds(1); - if (AudioToPlay.Count >= 1) - Timing.RunCoroutine(Playback(0)); - } + Log.Error($"Failed to retrieve audio {webRequest.responseCode} {webRequest.downloadHandler.text}"); + + if (!Continue || AudioToPlay.Count < 1) yield break; + + yield return Timing.WaitForSeconds(1); + + if (AudioToPlay.Count >= 1) + Timing.RunCoroutine(Playback(0)); + yield break; } - CurrentPlayStream = new MemoryStream(www.downloadHandler.data); + CurrentPlayStream = new MemoryStream(webRequest.downloadHandler.data); } else { @@ -259,129 +173,216 @@ public virtual IEnumerator Playback(int position) { Log.Error($"Audio file {CurrentPlay} is not valid. Audio files must be ogg files"); yield return Timing.WaitForSeconds(1); + if (AudioToPlay.Count >= 1) Timing.RunCoroutine(Playback(0)); + yield break; } + CurrentPlayStream = new MemoryStream(File.ReadAllBytes(CurrentPlay)); } else { Log.Error($"Audio file {CurrentPlay} does not exist. skipping."); yield return Timing.WaitForSeconds(1); + if (AudioToPlay.Count >= 1) Timing.RunCoroutine(Playback(0)); + yield break; } } - + CurrentPlayStream.Seek(0, SeekOrigin.Begin); - - VorbisReader = new NVorbis.VorbisReader(CurrentPlayStream); + VorbisReader = new VorbisReader(CurrentPlayStream); if (VorbisReader.Channels >= 2) { Log.Error($"Audio file {CurrentPlay} is not valid. Audio files must be mono."); yield return Timing.WaitForSeconds(1); + if (AudioToPlay.Count >= 1) Timing.RunCoroutine(Playback(0)); + VorbisReader.Dispose(); CurrentPlayStream.Dispose(); + yield break; } - + if (VorbisReader.SampleRate != 48000) { Log.Error($"Audio file {CurrentPlay} is not valid. Audio files must have a SamepleRate of 48000"); yield return Timing.WaitForSeconds(1); + if (AudioToPlay.Count >= 1) Timing.RunCoroutine(Playback(0)); + VorbisReader.Dispose(); CurrentPlayStream.Dispose(); + yield break; } - OnTrackLoaded?.Invoke(this, index == -1, index, CurrentPlay); - Log.Info($"Playing {CurrentPlay} with samplerate of {VorbisReader.SampleRate}"); + + Track.InvokeTrackLoadedEvent(this, index == -1, index, CurrentPlay); + Log.Debug($"Playing {CurrentPlay} with samplerate of {VorbisReader.SampleRate}"); + IsPlaying = true; + samplesPerSecond = VoiceChatSettings.SampleRate * VoiceChatSettings.Channels; //_samplesPerSecond = VorbisReader.Channels * VorbisReader.SampleRate / 5; SendBuffer = new float[samplesPerSecond / 5 + HeadSamples]; ReadBuffer = new float[samplesPerSecond / 5 + HeadSamples]; - int cnt; - while ((cnt = VorbisReader.ReadSamples(ReadBuffer, 0, ReadBuffer.Length)) > 0) + + while (VorbisReader.ReadSamples(ReadBuffer, 0, ReadBuffer.Length) > 0) { if (stopTrack) { VorbisReader.SeekTo(VorbisReader.TotalSamples - 1); stopTrack = false; } - while (!ShouldPlay) - { - yield return Timing.WaitForOneFrame; - } + + while (!ShouldPlay) yield return Timing.WaitForOneFrame; + while (StreamBuffer.Count >= ReadBuffer.Length) { ready = true; yield return Timing.WaitForOneFrame; } - for (int i = 0; i < ReadBuffer.Length; i++) - { - StreamBuffer.Enqueue(ReadBuffer[i]); - } - } - Log.Info($"Track Complete."); - int nextQueuepos = 0; - if (Continue && Loop && index == -1) - { - nextQueuepos = -1; - Timing.RunCoroutine(Playback(nextQueuepos)); - OnFinishedTrack?.Invoke(this, CurrentPlay, index == -1, ref nextQueuepos); - yield break; + for (var i = 0; i < ReadBuffer.Length; i++) StreamBuffer.Enqueue(ReadBuffer[i]); } - if (Continue && AudioToPlay.Count >= 1) + Log.Debug("Track Complete."); + IsPlaying = false; + + var nextQueuePos = 0; + + switch (Continue) { - Timing.RunCoroutine(Playback(nextQueuepos)); - OnFinishedTrack?.Invoke(this, CurrentPlay, index == -1, ref nextQueuepos); - yield break; + case true when Loop && index == -1: + nextQueuePos = -1; + + Timing.RunCoroutine(Playback(nextQueuePos)); + Track.InvokeFinishedTrackEvent(this, CurrentPlay, false, ref nextQueuePos); + + yield break; + case true when AudioToPlay.Count >= 1: + Timing.RunCoroutine(Playback(nextQueuePos)); + Track.InvokeFinishedTrackEvent(this, CurrentPlay, index == -1, ref nextQueuePos); + + yield break; + default: + Track.InvokeFinishedTrackEvent(this, CurrentPlay, index == -1, ref nextQueuePos); + break; } - - OnFinishedTrack?.Invoke(this, CurrentPlay, index == -1, ref nextQueuepos); } - public virtual void Update() - { - if (Owner == null || !ready || StreamBuffer.Count == 0 || !ShouldPlay) return; + #region Internal - allowedSamples += Time.deltaTime * samplesPerSecond; - int toCopy = Mathf.Min(Mathf.FloorToInt(allowedSamples), StreamBuffer.Count); - if (LogDebug) - Log.Debug($"1 {toCopy} {allowedSamples} {samplesPerSecond} {StreamBuffer.Count} {PlaybackBuffer.Length} {PlaybackBuffer.WriteHead}"); - if (toCopy > 0) - { - for (int i = 0; i < toCopy; i++) - { - PlaybackBuffer.Write(StreamBuffer.Dequeue() * (Volume / 100f)); - } - } + public const int HeadSamples = 1920; - if (LogDebug) - Log.Debug($"2 {toCopy} {allowedSamples} {samplesPerSecond} {StreamBuffer.Count} {PlaybackBuffer.Length} {PlaybackBuffer.WriteHead}"); - - allowedSamples -= toCopy; + public OpusEncoder Encoder { get; } = new OpusEncoder(OpusApplicationType.Voip); - while (PlaybackBuffer.Length >= 480) - { - PlaybackBuffer.ReadTo(SendBuffer, (long)480, 0L); - int dataLen = Encoder.Encode(SendBuffer, EncodedBuffer, 480); - - foreach (var plr in ReferenceHub.AllHubs) - { - if (plr.connectionToClient == null || (BroadcastTo.Count >= 1 && !BroadcastTo.Contains(plr.PlayerId))) continue; - - plr.connectionToClient.Send(new VoiceMessage(Owner, BroadcastChannel, EncodedBuffer, dataLen, false)); - } - } + public PlaybackBuffer PlaybackBuffer { get; } = new PlaybackBuffer(); + + public byte[] EncodedBuffer { get; } = new byte[512]; + + public bool stopTrack; + + public bool ready; + + public CoroutineHandle PlaybackCoroutine; + + public float allowedSamples; + + public int samplesPerSecond; + + public Queue StreamBuffer { get; } = new Queue(); + + public VorbisReader VorbisReader { get; set; } + + public float[] SendBuffer { get; set; } + + public float[] ReadBuffer { get; set; } + + public bool IsPlaying { get; private set; } + + #endregion + + #region AudioPlayer Settings + + /// + /// The ReferenceHub instance that this player sends as. + /// + public ReferenceHub Owner { get; set; } + + /// + /// Volume that the player will play at. + /// + public float Volume { get; set; } = 100f; + + /// + /// List of Paths/Urls that the player will play from (Urls only work if is true) + /// + public List AudioToPlay = new List(); + + /// + /// Path/Url of the currently playing audio file. + /// + public string CurrentPlay; + + /// + /// Stream containing the Audio data + /// + public MemoryStream CurrentPlayStream; + + /// + /// Boolean indicating whether or not the Queue will loop (Audio will be added to the end of the queue after it gets + /// removed on play) + /// + public bool Loop; + + /// + /// If the playlist should be shuffled when an audio track start. + /// + public bool Shuffle; + + /// + /// Whether the Player should continue playing by itself after the current Track ends. + /// + public bool Continue = true; + + /// + /// Whether the Player should be sending audio to the broadcaster. + /// + public bool ShouldPlay = true; + + /// + /// If URLs are allowed to be played + /// + public bool AllowUrl; + + /// + /// If Vebose logs shouldbe shown (Note: can be very spammy) + /// + public bool VerboseLogs; + + /// + /// If not empty, the audio will only be sent to players with the PlayerIds in this list + /// + public List BroadcastTo = new List(); + + static AudioPlayerBase() + { + AudioPlayers = new Dictionary(); } + + /// + /// Gets or Sets the Channel where audio will be played in + /// + public VoiceChatChannel BroadcastChannel { get; set; } = VoiceChatChannel.Proximity; + + #endregion } } \ No newline at end of file diff --git a/SCPSLAudioApi/Events/Handlers/Track.cs b/SCPSLAudioApi/Events/Handlers/Track.cs new file mode 100644 index 0000000..923bbc5 --- /dev/null +++ b/SCPSLAudioApi/Events/Handlers/Track.cs @@ -0,0 +1,74 @@ +using SCPSLAudioApi.AudioCore; + +namespace SCPSLAudioApi.Events.Handlers +{ + public class Track + { + /// + /// Fired when a track finishes. + /// + /// The AudioPlayer instance that this event fired for + /// The track the AudioPlayer was playing + /// If the AudioPlayer was playing Directly (-1 index) + /// Position in the Queue that will play next, can be set to a different value + public delegate void TrackFinished(AudioPlayerBase playerBase, string track, bool directPlay, + ref int nextQueuePos); + + /// + /// Fired when a track is loaded and will begin playing. + /// + /// The AudioPlayer instance that this event fired for + /// If the AudioPlayer was playing Directly (-1 index) + /// Position in the Queue that will play + /// The track the AudioPlayer will play + public delegate void TrackLoaded(AudioPlayerBase playerBase, bool directPlay, int queuePos, string track); + + /// + /// Fired when a track has been selected + /// + /// The AudioPlayer instance that this event fired for + /// If the AudioPlayer was playing Directly (-1 index) + /// Position in the Queue of the track that will start + /// The track the AudioPlayer will play + public delegate void TrackSelected(AudioPlayerBase playerBase, bool directPlay, int queuePos, ref string track); + + /// + /// Fired when a track is getting selected. + /// + /// The AudioPlayer instance that this event fired for + /// If the AudioPlayer was playing Directly (-1 index) + /// Position in the Queue of the track that is going to be selected + public delegate void TrackSelecting(AudioPlayerBase playerBase, bool directPlay, ref int queuePos); + + public static event TrackSelecting OnTrackSelecting; + + public static void InvokeTrackSelectingEvent(AudioPlayerBase playerBase, bool directPlay, ref int queuePos) + { + OnTrackSelecting?.Invoke(playerBase, directPlay, ref queuePos); + } + + public static event TrackSelected OnTrackSelected; + + public static void InvokeTrackSelectedEvent(AudioPlayerBase playerBase, bool directPlay, int queuePos, + ref string track) + { + OnTrackSelected?.Invoke(playerBase, directPlay, queuePos, ref track); + } + + public static event TrackLoaded OnTrackLoaded; + + public static void InvokeTrackLoadedEvent(AudioPlayerBase playerBase, bool directPlay, int queuePos, + string track) + { + OnTrackLoaded?.Invoke(playerBase, directPlay, queuePos, track); + } + + public static event TrackFinished OnFinishedTrack; + + public static void InvokeFinishedTrackEvent(AudioPlayerBase playerBase, string track, bool directPlay, + ref int nextQueuePos) + { + OnFinishedTrack?.Invoke(playerBase, track, directPlay, ref nextQueuePos); + } + } +} \ No newline at end of file diff --git a/SCPSLAudioApi/FodyWeavers.xml b/SCPSLAudioApi/FodyWeavers.xml index 8f80e10..0034489 100644 --- a/SCPSLAudioApi/FodyWeavers.xml +++ b/SCPSLAudioApi/FodyWeavers.xml @@ -2,6 +2,7 @@ + System.Memory NVorbis diff --git a/SCPSLAudioApi/SCPSLAudioApi.csproj b/SCPSLAudioApi/SCPSLAudioApi.csproj index 9df2118..127e79a 100644 --- a/SCPSLAudioApi/SCPSLAudioApi.csproj +++ b/SCPSLAudioApi/SCPSLAudioApi.csproj @@ -42,182 +42,217 @@ --> - - - + + ..\packages\EXILED.7.0.0-rc.2-1\lib\net48\Assembly-CSharp-Publicized.dll + + + + ..\packages\EXILED.7.0.0-rc.2-1\lib\net48\CommandSystem.Core.dll + - ..\packages\Costura.Fody.5.8.0-alpha0098\lib\netstandard1.0\Costura.dll - True + ..\packages\Costura.Fody.5.8.0-alpha0098\lib\netstandard1.0\Costura.dll + True + + + ..\packages\EXILED.7.0.0-rc.2-1\lib\net48\Exiled.API.dll + + + ..\packages\EXILED.7.0.0-rc.2-1\lib\net48\Exiled.CreditTags.dll + + + ..\packages\EXILED.7.0.0-rc.2-1\lib\net48\Exiled.CustomItems.dll + + + ..\packages\EXILED.7.0.0-rc.2-1\lib\net48\Exiled.CustomRoles.dll + + + ..\packages\EXILED.7.0.0-rc.2-1\lib\net48\Exiled.Events.dll + + + ..\packages\EXILED.7.0.0-rc.2-1\lib\net48\Exiled.Loader.dll + + + ..\packages\EXILED.7.0.0-rc.2-1\lib\net48\Exiled.Permissions.dll + + + ..\packages\EXILED.7.0.0-rc.2-1\lib\net48\Exiled.Updater.dll - ..\packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll - True + ..\packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll + True - + - + + ..\packages\EXILED.7.0.0-rc.2-1\lib\net48\NorthwoodLib.dll + - ..\packages\NVorbis.0.10.5\lib\net45\NVorbis.dll - True + ..\packages\NVorbis.0.10.5\lib\net45\NVorbis.dll + True + + + ..\packages\EXILED.7.0.0-rc.2-1\lib\net48\PluginAPI.dll - ..\packages\System.AppContext.4.3.0\lib\net463\System.AppContext.dll - True + ..\packages\System.AppContext.4.3.0\lib\net463\System.AppContext.dll + True - ..\packages\System.Buffers.4.4.0\lib\netstandard2.0\System.Buffers.dll - True + ..\packages\System.Buffers.4.4.0\lib\netstandard2.0\System.Buffers.dll + True - ..\packages\System.Console.4.3.0\lib\net46\System.Console.dll - True + ..\packages\System.Console.4.3.0\lib\net46\System.Console.dll + True - ..\packages\System.Diagnostics.DiagnosticSource.4.3.0\lib\net46\System.Diagnostics.DiagnosticSource.dll - True + ..\packages\System.Diagnostics.DiagnosticSource.4.3.0\lib\net46\System.Diagnostics.DiagnosticSource.dll + True - ..\packages\System.Diagnostics.Tracing.4.3.0\lib\net462\System.Diagnostics.Tracing.dll - True + ..\packages\System.Diagnostics.Tracing.4.3.0\lib\net462\System.Diagnostics.Tracing.dll + True - ..\packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll - True + ..\packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll + True - ..\packages\System.IO.4.3.0\lib\net462\System.IO.dll - True + ..\packages\System.IO.4.3.0\lib\net462\System.IO.dll + True - ..\packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll - True + ..\packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll + True - ..\packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll - True + ..\packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll + True - ..\packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll - True + ..\packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll + True - ..\packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll - True + ..\packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll + True - ..\packages\System.Linq.4.3.0\lib\net463\System.Linq.dll - True + ..\packages\System.Linq.4.3.0\lib\net463\System.Linq.dll + True - ..\packages\System.Linq.Expressions.4.3.0\lib\net463\System.Linq.Expressions.dll - True + ..\packages\System.Linq.Expressions.4.3.0\lib\net463\System.Linq.Expressions.dll + True - ..\packages\System.Memory.4.5.3\lib\netstandard2.0\System.Memory.dll - True + ..\packages\System.Memory.4.5.3\lib\netstandard2.0\System.Memory.dll + True - ..\packages\System.Net.Http.4.3.0\lib\net46\System.Net.Http.dll - True + ..\packages\System.Net.Http.4.3.0\lib\net46\System.Net.Http.dll + True - ..\packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll - True + ..\packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll + True - ..\packages\System.Numerics.Vectors.4.4.0\lib\net46\System.Numerics.Vectors.dll - True + ..\packages\System.Numerics.Vectors.4.4.0\lib\net46\System.Numerics.Vectors.dll + True - ..\packages\System.Reflection.4.3.0\lib\net462\System.Reflection.dll - True + ..\packages\System.Reflection.4.3.0\lib\net462\System.Reflection.dll + True - ..\packages\System.Runtime.4.3.0\lib\net462\System.Runtime.dll - True + ..\packages\System.Runtime.4.3.0\lib\net462\System.Runtime.dll + True - ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll - True + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + True - ..\packages\System.Runtime.Extensions.4.3.0\lib\net462\System.Runtime.Extensions.dll - True + ..\packages\System.Runtime.Extensions.4.3.0\lib\net462\System.Runtime.Extensions.dll + True - ..\packages\System.Runtime.InteropServices.4.3.0\lib\net463\System.Runtime.InteropServices.dll - True + ..\packages\System.Runtime.InteropServices.4.3.0\lib\net463\System.Runtime.InteropServices.dll + True - ..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll - True + ..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll + True - ..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net463\System.Security.Cryptography.Algorithms.dll - True + ..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net463\System.Security.Cryptography.Algorithms.dll + True - ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll - True + ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll + True - ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll - True + ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll + True - ..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net461\System.Security.Cryptography.X509Certificates.dll - True + ..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net461\System.Security.Cryptography.X509Certificates.dll + True - ..\packages\System.Text.RegularExpressions.4.3.0\lib\net463\System.Text.RegularExpressions.dll - True + ..\packages\System.Text.RegularExpressions.4.3.0\lib\net463\System.Text.RegularExpressions.dll + True - ..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll - True + ..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll + True - ..\packages\System.Xml.ReaderWriter.4.3.0\lib\net46\System.Xml.ReaderWriter.dll - True - - - - - - + ..\packages\System.Xml.ReaderWriter.4.3.0\lib\net46\System.Xml.ReaderWriter.dll + True + + + + + + ..\packages\EXILED.7.0.0-rc.2-1\lib\net48\YamlDotNet.dll + - - + + + - - + + - - + + - - This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. - - - - + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. + + + + diff --git a/SCPSLAudioApi/packages.config b/SCPSLAudioApi/packages.config index 0230c6d..662dbcc 100644 --- a/SCPSLAudioApi/packages.config +++ b/SCPSLAudioApi/packages.config @@ -1,6 +1,7 @@  +