Skip to content

Commit

Permalink
Added Volume Cues:
Browse files Browse the repository at this point in the history
 - Added volume cues, allowing the volume of a sound cue to be faded after it's started, fixes #13
 - Added new fade curves
 - Bugfixes related to fading
 - Fixed path resolution bugs
  • Loading branch information
space928 committed Mar 1, 2024
1 parent d8f792d commit 7143bba
Show file tree
Hide file tree
Showing 13 changed files with 482 additions and 71 deletions.
19 changes: 19 additions & 0 deletions QPlayer/Models/ShowFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public enum CueType
SoundCue,
TimeCodeCue,
StopCue,
VolumeCue
}

public enum LoopMode
Expand All @@ -60,6 +61,7 @@ public enum StopMode
[JsonDerivedType(typeof(SoundCue), typeDiscriminator: nameof(SoundCue))]
[JsonDerivedType(typeof(TimeCodeCue), typeDiscriminator: nameof(TimeCodeCue))]
[JsonDerivedType(typeof(StopCue), typeDiscriminator: nameof(StopCue))]
[JsonDerivedType(typeof(VolumeCue), typeDiscriminator: nameof(VolumeCue))]
public record Cue
{
public CueType type;
Expand Down Expand Up @@ -105,6 +107,7 @@ public record SoundCue : Cue
public float volume = 1;
public float fadeIn;
public float fadeOut;
public FadeType fadeType;

public SoundCue() : base()
{
Expand Down Expand Up @@ -132,10 +135,26 @@ public record StopCue : Cue
public decimal stopQid;
public StopMode stopMode;
public float fadeOutTime;
public FadeType fadeType;

public StopCue() : base()
{
type = CueType.StopCue;
}
}

[Serializable]
[JsonDerivedType(typeof(VolumeCue), typeDiscriminator: nameof(VolumeCue))]
public record VolumeCue : Cue
{
public decimal soundQid;
public float fadeTime;
public float volume;
public FadeType fadeType;

public VolumeCue() : base()
{
type = CueType.VolumeCue;
}
}
}
2 changes: 1 addition & 1 deletion QPlayer/QPlayer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<Title>QPlayer</Title>
<Version>1.3.3-beta</Version>
<Version>1.4.1-beta</Version>
<Authors>Thomas Mathieson</Authors>
<Company>Thomas Mathieson</Company>
<Copyright>©️ Thomas Mathieson 2024</Copyright>
Expand Down
Binary file modified QPlayer/Resources/Icons/ConvertedIcons.xaml
Binary file not shown.
1 change: 1 addition & 0 deletions QPlayer/Resources/Icons/volume-low-solid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions QPlayer/ThemesV2/Icons.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<DrawingImage x:Key="IconDummyCue" Drawing="{StaticResource Icon_circle_solidDrawingGroup}"/>
<DrawingImage x:Key="IconSoundCue" Drawing="{StaticResource Icon_music_solidDrawingGroup}"/>
<DrawingImage x:Key="IconTimeCodeCue" Drawing="{StaticResource Icon_clock_solidDrawingGroup}"/>
<DrawingImage x:Key="IconVolumeCue" Drawing="{StaticResource Icon_volume_low_solidDrawingGroup}"/>

<DrawingImage x:Key="IconOneShot" Drawing="{StaticResource Icon_arrow_right_long_solidDrawingGroup}"/>
<DrawingImage x:Key="IconLoop" Drawing="{StaticResource Icon_rotate_solidDrawingGroup}"/>
Expand Down
9 changes: 9 additions & 0 deletions QPlayer/ViewModels/CueViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,11 @@ public abstract class CueViewModel : ObservableObject, IConvertibleModel<Cue, Cu
[Reactive] public static ObservableCollection<CueType>? CueTypeVals { get; private set; }
[Reactive] public static ObservableCollection<LoopMode>? LoopModeVals { get; private set; }
[Reactive] public static ObservableCollection<StopMode>? StopModeVals { get; private set; }
[Reactive] public static ObservableCollection<FadeType>? FadeTypeVals { get; private set; }
#endregion

public event EventHandler? OnCompleted;

protected SynchronizationContext? synchronizationContext;
protected CueViewModel? parent;
protected Cue? cueModel;
Expand All @@ -147,6 +150,7 @@ public CueViewModel(MainViewModel mainViewModel)
CueTypeVals ??= new ObservableCollection<CueType>(Enum.GetValues<CueType>());
LoopModeVals ??= new ObservableCollection<LoopMode>(Enum.GetValues<LoopMode>());
StopModeVals ??= new ObservableCollection<StopMode>(Enum.GetValues<StopMode>());
FadeTypeVals ??= new ObservableCollection<FadeType>(Enum.GetValues<FadeType>());
}

private void MainViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
Expand Down Expand Up @@ -190,7 +194,10 @@ public virtual void DelayedGo()
public virtual void Go()
{
if (Duration == TimeSpan.Zero)
{
OnCompleted?.Invoke(this, EventArgs.Empty);
return;
}
State = CueState.Playing;
if(!mainViewModel?.ActiveCues?.Contains(this) ?? false)
mainViewModel?.ActiveCues.Add(this);
Expand All @@ -215,6 +222,7 @@ public virtual void Stop()
goTimer.Stop();
State = CueState.Ready;
mainViewModel?.ActiveCues.Remove(this);
OnCompleted?.Invoke(this, EventArgs.Empty);
}

/// <summary>
Expand Down Expand Up @@ -312,6 +320,7 @@ public static CueViewModel FromModel(Cue cue, MainViewModel mainViewModel)
case CueType.SoundCue: viewModel = SoundCueViewModel.FromModel(cue, mainViewModel); break;
case CueType.TimeCodeCue: viewModel = TimeCodeCueViewModel.FromModel(cue, mainViewModel); break;
case CueType.StopCue: viewModel = StopCueViewModel.FromModel(cue, mainViewModel); break;
case CueType.VolumeCue: viewModel = VolumeCueViewModel.FromModel(cue, mainViewModel); break;
default: throw new ArgumentException(null, nameof(cue.type));
}
viewModel.mainViewModel = mainViewModel;
Expand Down
188 changes: 188 additions & 0 deletions QPlayer/ViewModels/FadingSampleProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
using NAudio.Wave;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace QPlayer.ViewModels
{
internal class FadingSampleProvider : ISampleProvider
{
private readonly ISampleProvider source;
private readonly object lockObj = new();
private FadeState state;
private long fadeTime;
private long fadeDuration;
private float startVolume = 1;
private float endVolume = 1;
private FadeType fadeType;
private Action<bool>? onCompleteAction;
private SynchronizationContext? synchronizationContext;

public FadingSampleProvider(ISampleProvider source, bool startSilent = false)
{
this.source = source;
state = FadeState.Ready;
if (startSilent)
startVolume = 0;
else
startVolume = 1;
}

public WaveFormat WaveFormat => source.WaveFormat;

public float Volume
{
get => startVolume;
set => startVolume = value;
}

public int Read(float[] buffer, int offset, int count)
{
int numSource = source.Read(buffer, offset, count);
int num = numSource;
lock (lockObj)
{
if (state == FadeState.Fading)
{
int numFaded = FadeSamples(buffer, offset, numSource);
offset += numFaded;
num -= numFaded;
}

if (startVolume == 0)
{
Array.Clear(buffer, offset, num);
return numSource;
} else if (startVolume == 1)
{
return numSource;
}

for (int x = offset; x < offset + num; x++)
buffer[x] *= startVolume;
}

return numSource;
}

/// <summary>
/// Starts a new fade operation, cancelling any active fade operation.
/// </summary>
/// <param name="volume">The volume to fade to</param>
/// <param name="durationMS">The time to fade over in milliseconds</param>
/// <param name="fadeType">The type of fade to use</param>
/// <param name="onComplete">Optionally, an event to raise when the fade is completed. <c>true</c> is passed to the
/// event handler if the fade completed normally, <c>false</c> if it was cancelled. The event is invoked on the
/// thread that called this method.</param>
public void BeginFade(float volume, double durationMS, FadeType fadeType = FadeType.Linear, Action<bool>? onComplete = null)
{
lock (lockObj)
{
EndFade();

fadeTime = 0;
fadeDuration = (int)(durationMS * source.WaveFormat.SampleRate / 1000.0);
endVolume = volume;
this.fadeType = fadeType;
onCompleteAction = onComplete;
synchronizationContext = SynchronizationContext.Current;
state = FadeState.Fading;
}
}

/// <summary>
/// Cancels the active fade operation.
/// </summary>
public void EndFade()
{
if (state != FadeState.Fading)
return;

lock (lockObj)
{
state = FadeState.Ready;
float t = GetFadeFraction();
startVolume = endVolume * t + startVolume * (1 - t);
if (synchronizationContext != null)
synchronizationContext.Post(x => onCompleteAction?.Invoke(false), null);
else
onCompleteAction?.Invoke(false);
//onCompleteAction = null;
//synchronizationContext = null;
}
}

private int FadeSamples(float[] buffer, int offset, int count)
{
int i = 0;
int channels = source.WaveFormat.Channels;
while (i < count)
{
if (fadeTime >= fadeDuration)
{
startVolume = endVolume;
state = FadeState.Ready;
if (synchronizationContext != null)
synchronizationContext.Post(x => onCompleteAction?.Invoke(true), null);
else
onCompleteAction?.Invoke(true);
//onCompleteAction = null;
//synchronizationContext = null;

break;
}

float t = GetFadeFraction();

for (int c = 0; c < channels; c++)
buffer[offset + i + c] *= endVolume * t + startVolume * (1 - t);

i += channels;
fadeTime++;
}

return i;
}

private float GetFadeFraction()
{
float t = fadeTime / (float)fadeDuration;
switch (fadeType)
{
case FadeType.SCurve:
// Cubic hermite spline
float t2 = t * t;
float t3 = t2 * t;
t = -2 * t3 + 3 * t2;
break;
case FadeType.Square:
t *= t;
break;
case FadeType.InverseSquare:
t = MathF.Sqrt(t);
break;
case FadeType.Linear:
default:
break;
}
return t;
}
}

internal enum FadeState
{
Ready,
Fading
}

public enum FadeType
{
Linear,
SCurve,
Square,
InverseSquare,
}
}
Loading

0 comments on commit 7143bba

Please sign in to comment.