Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ For reference on the generated code, see [generated source repository](https://g

## License

This project is licensed under the MIT license. Please see [the license file](LICENSE) for more information. tl;dr you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source.
This project is licensed under the MIT license. Please see [the license file](LICENSE) for more information. tl;dr you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source.

The fonts used as the default font in this framework are licensed under [SIL Open Font License 1.1](https://openfontlicense.org).
20 changes: 20 additions & 0 deletions Sakura.Framework/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
using Sakura.Framework.Logging;
using Sakura.Framework.Platform;
using Sakura.Framework.Reactive;
using Sakura.Framework.Timing;

namespace Sakura.Framework;

Expand Down Expand Up @@ -60,9 +61,12 @@ public class App : Container, IFocusManager, IDisposable
private DrawVisualiser drawVisualiser;
private GlobalStatisticsDisplay globalStatisticsDisplay;
private TextureViewerDisplay textureViewerDisplay;
private AudioMixerVisualiser audioMixerVisualiser;

public override void Load()
{
Clock = new FramedClock(Host.AppClock, true);

base.Load();

AudioManager = CreateAudioManager();
Expand Down Expand Up @@ -129,6 +133,11 @@ public override void Load()
Depth = float.MaxValue - 10,
Alpha = 0
});
Add(audioMixerVisualiser = new AudioMixerVisualiser(AudioManager)
{
Depth = float.MaxValue - 10,
Alpha = 0
});
Add(FpsGraph = new FpsGraph(Host.AppClock)
{
Depth = float.MaxValue
Expand Down Expand Up @@ -162,6 +171,11 @@ private void toggleTextureViewerDisplay()
textureViewerDisplay.ToggleVisibility();
}

private void toggleAudioMixerVisualiserDisplay()
{
audioMixerVisualiser.ToggleVisibility();
}

/// <summary>
/// Create a default <see cref="Storage"/>
/// </summary>
Expand Down Expand Up @@ -191,6 +205,7 @@ public override void Update()
{
base.Update();
AudioManager?.Update(Clock.ElapsedFrameTime);
Scheduler.Update();
}

public override bool OnKeyDown(KeyEvent e)
Expand All @@ -210,6 +225,11 @@ public override bool OnKeyDown(KeyEvent e)
toggleTextureViewerDisplay();
return true;
}
else if (!e.IsRepeat && e.Key == Key.F9 && (e.Modifiers & KeyModifiers.Control) > 0)
{
toggleAudioMixerVisualiserDisplay();
return true;
}
if (!e.IsRepeat && e.Key == Key.F11 && (e.Modifiers & KeyModifiers.Control) > 0)
{
showFpsGraph.Value = !showFpsGraph.Value;
Expand Down
3 changes: 3 additions & 0 deletions Sakura.Framework/Audio/AudioChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ public double RestartPoint
set => restartPoint = Math.Clamp(value, 0, Length);
}

public float AmplitudeLeft => 0;
public float AmplitudeRight => 0;

public double Length { get; protected set; }
public bool Looping { get; set; }

Expand Down
17 changes: 17 additions & 0 deletions Sakura.Framework/Audio/AudioChannelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Threading.Tasks;
using Sakura.Framework.Audio.BassEngine;

namespace Sakura.Framework.Audio;

Expand Down Expand Up @@ -85,4 +86,20 @@ public static async Task CrossfadeToAsync(this IAudioChannel currentChannel, IAu
Task fadeInTask = nextChannel?.FadeInAsync(duration, targetVolume) ?? Task.CompletedTask;
await Task.WhenAll(fadeOutTask, fadeInTask);
}

/// <summary>
/// Applies a reactive Low-Pass filter to the target audio channel.
/// </summary>
/// <returns>The <see cref="BassLowPassFilter"/> instance to control the cutoff frequency.</returns>
public static BassLowPassFilter AddLowPassFilter(this IAudioChannel channel)
{
if (channel is BassAudioChannel bassChannel)
{
// Attach the effect directly to the internal BASS handle
return new BassLowPassFilter(bassChannel.ChannelHandle);
}

// Return null or a dummy filter if running in Headless mode
return null;
}
}
102 changes: 102 additions & 0 deletions Sakura.Framework/Audio/AudioDucker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// This code is part of the Sakura framework project. Licensed under the MIT License.
// See the LICENSE file for full license text.

using System;
using ManagedBass;
using Sakura.Framework.Audio.BassEngine;
using Sakura.Framework.Graphics.Drawables;
using Sakura.Framework.Reactive;

namespace Sakura.Framework.Audio;

/// <summary>
/// A component that monitors a source audio channel and duck the volume
/// of a target audio channel based on the source's output level.
/// </summary>
public class AudioDucker : Component
{
private readonly IAudioChannel source;
private readonly IAudioChannel target;

private readonly Reactive<double> targetVolumeBindable;

/// <summary>
/// How loud the source must be (0.0 to 1.0) to trigger maximum ducking.
/// </summary>
public float Threshold { get; set; } = 0.1f;

/// <summary>
/// The maximum amount to multiply the target's volume by when fully ducked (e.g., 0.3 = 30% volume).
/// </summary>
public double DuckMultiplier { get; set; } = 0.3;

/// <summary>
/// How fast the volume recovers when the source goes quiet.
/// </summary>
public float RecoverySpeed { get; set; } = 0.05f;

private double currentDuckFactor = 1.0;

public AudioDucker(IAudioChannel source, IAudioChannel target, Reactive.Reactive<double> targetVolumeBindable)
{
this.source = source;
this.target = target;
this.targetVolumeBindable = targetVolumeBindable;

AlwaysPresent = true; // Ensure this updates even if hidden
}

public override void Update()
{
base.Update();

if (source is BassAudioChannel bassSource)
{
// Get the current output level of the source channel
int level = Bass.ChannelGetLevel(bassSource.ChannelHandle);

if (level != -1)
{
// BASS packs left channel in low word, right channel in high word
int left = level & 0xFFFF;
int right = level >> 16;

// Calculate the peak level from 0.0 to 1.0
float peak = Math.Max(left, right) / 32768f;

// Determine the target duck factor based on the threshold
double targetDuckFactor = 1.0;
if (peak > Threshold)
{
// If it's loud, snap the duck factor down
targetDuckFactor = DuckMultiplier;
}

// Smoothly ease the current duck factor towards the target
if (currentDuckFactor > targetDuckFactor)
{
// Attack instantly
currentDuckFactor = targetDuckFactor;
}
else
{
// Recover smoothly over time (incorporate Clock for frame-rate independence)
currentDuckFactor += RecoverySpeed * (Clock.ElapsedFrameTime / 16.66f);
currentDuckFactor = Math.Min(1.0, currentDuckFactor);
}

// Apply the ducked multiplier to the original volume value
// Note: We bypass the Value setter to avoid triggering a feedback loop if needed,
// but since we are writing to the channel directly here:
if (target is BassAudioChannel bassTarget)
{
// Calculate the final volume: (Framework Target Volume) * (Ducking Factor)
double finalVolume = targetVolumeBindable.Value * currentDuckFactor;

// Apply directly to BASS to avoid mutating the user's Reactive value
Bass.ChannelSetAttribute(bassTarget.ChannelHandle, ChannelAttribute.Volume, (float)finalVolume);
}
}
}
}
}
2 changes: 2 additions & 0 deletions Sakura.Framework/Audio/AudioManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ internal class AudioManager : IAudioManager
public Reactive<double> MasterVolume { get; } = new Reactive<double>(1.0);
public Reactive<double> TrackVolume { get; } = new Reactive<double>(1.0);
public Reactive<double> SampleVolume { get; } = new Reactive<double>(1.0);
public IAudioMixer TrackMixer { get; } = new AudioMixer();
public IAudioMixer SampleMixer { get; } = new AudioMixer();

public ITrack CreateTrack(Stream stream)
{
Expand Down
70 changes: 70 additions & 0 deletions Sakura.Framework/Audio/AudioMixer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// This code is part of the Sakura framework project. Licensed under the MIT License.
// See the LICENSE file for full license text.

using System;
using System.Collections.Generic;
using Sakura.Framework.Reactive;

namespace Sakura.Framework.Audio;

/// <summary>
/// A base or dummy implementation of an audio mixer.
/// </summary>
public class AudioMixer : IAudioMixer
{
protected readonly List<IAudioChannel> MixedChannels = new List<IAudioChannel>();

public event Action OnStart = delegate { };
public event Action OnStop = delegate { };
public event Action OnEnd = delegate { };

public ReactiveBool IsRunning { get; } = new ReactiveBool();
public Reactive<double> Volume { get; } = new Reactive<double>(1.0);
public Reactive<double> Frequency { get; } = new Reactive<double>(1.0);
public Reactive<double> Balance { get; } = new Reactive<double>(0.0);

public double CurrentTime { get; set; }
public double Length => 0;
public double RestartPoint { get; set; }
public float AmplitudeLeft { get; } = 0;
public float AmplitudeRight { get; } = 0;
public bool Looping { get; set; }
public bool AutoDispose { get; set; }

public virtual void Play()
{
IsRunning.Value = true;
OnStart.Invoke();
}

public virtual void Pause()
{
IsRunning.Value = false;
OnStop.Invoke();
}

public virtual void Stop()
{
IsRunning.Value = false;
CurrentTime = 0;
OnStop.Invoke();
}

public IEnumerable<IAudioChannel> ActiveChannels => MixedChannels;

public virtual void AddChannel(IAudioChannel channel)
{
if (!MixedChannels.Contains(channel))
MixedChannels.Add(channel);
}

public virtual void RemoveChannel(IAudioChannel channel)
{
MixedChannels.Remove(channel);
}

public virtual void Dispose()
{
MixedChannels.Clear();
}
}
Loading