// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Mixing; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Game.Beatmaps; namespace osu.Game.Audio { public class PreviewTrackManager : Component { private readonly BindableDouble muteBindable = new BindableDouble(); [Resolved] private AudioManager audio { get; set; } private PreviewTrackStore trackStore; protected TrackManagerPreviewTrack CurrentTrack; private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(OsuGameBase.GLOBAL_TRACK_VOLUME_ADJUST); [BackgroundDependencyLoader] private void load(AudioManager audioManager) { // this is a temporary solution to get around muting ourselves. // todo: update this once we have a BackgroundTrackManager or similar. trackStore = new PreviewTrackStore(audioManager.TrackMixer, new OnlineStore()); audio.AddItem(trackStore); trackStore.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust); trackStore.AddAdjustment(AdjustableProperty.Volume, audio.VolumeTrack); } /// /// Retrieves a for a . /// /// The to retrieve the preview track for. /// The playable . public PreviewTrack Get(IBeatmapSetInfo beatmapSetInfo) { var track = CreatePreviewTrack(beatmapSetInfo, trackStore); track.Started += () => Schedule(() => { CurrentTrack?.Stop(); CurrentTrack = track; audio.Tracks.AddAdjustment(AdjustableProperty.Volume, muteBindable); }); track.Stopped += () => Schedule(() => { if (CurrentTrack != track) return; CurrentTrack = null; audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, muteBindable); }); return track; } /// /// Stops any currently playing . /// /// /// Only the immediate owner (an object that implements ) of the playing /// can globally stop the currently playing . The object holding a reference to the /// can always stop the themselves through . /// /// The which may be the owner of the . public void StopAnyPlaying(IPreviewTrackOwner source) { if (CurrentTrack == null || (CurrentTrack.Owner != null && CurrentTrack.Owner != source)) return; CurrentTrack.Stop(); // CurrentTrack should not be set to null here as it will result in incorrect handling in the track.Stopped callback above. } /// /// Creates the . /// protected virtual TrackManagerPreviewTrack CreatePreviewTrack(IBeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) => new TrackManagerPreviewTrack(beatmapSetInfo, trackStore); public class TrackManagerPreviewTrack : PreviewTrack { [Resolved(canBeNull: true)] public IPreviewTrackOwner Owner { get; private set; } private readonly IBeatmapSetInfo beatmapSetInfo; private readonly ITrackStore trackManager; public TrackManagerPreviewTrack(IBeatmapSetInfo beatmapSetInfo, ITrackStore trackManager) { this.beatmapSetInfo = beatmapSetInfo; this.trackManager = trackManager; } protected override void LoadComplete() { base.LoadComplete(); Logger.Log($"A {nameof(PreviewTrack)} was created without a containing {nameof(IPreviewTrackOwner)}. An owner should be added for correct behaviour."); } protected override Track GetTrack() => trackManager.Get($"https://b.ppy.sh/preview/{beatmapSetInfo.OnlineID}.mp3"); } private class PreviewTrackStore : AudioCollectionManager, ITrackStore { private readonly AudioMixer defaultMixer; private readonly IResourceStore store; internal PreviewTrackStore(AudioMixer defaultMixer, IResourceStore store) { this.defaultMixer = defaultMixer; this.store = store; } public Track GetVirtual(double length = double.PositiveInfinity) { if (IsDisposed) throw new ObjectDisposedException($"Cannot retrieve items for an already disposed {nameof(PreviewTrackStore)}"); var track = new TrackVirtual(length); AddItem(track); return track; } public Track Get(string name) { if (IsDisposed) throw new ObjectDisposedException($"Cannot retrieve items for an already disposed {nameof(PreviewTrackStore)}"); if (string.IsNullOrEmpty(name)) return null; var dataStream = store.GetStream(name); if (dataStream == null) return null; // Todo: This is quite unsafe. TrackBass shouldn't be exposed as public. Track track = new TrackBass(dataStream); defaultMixer.Add(track); AddItem(track); return track; } public Task GetAsync(string name) => Task.Run(() => Get(name)); public Stream GetStream(string name) => store.GetStream(name); public IEnumerable GetAvailableResources() => store.GetAvailableResources(); } } }