Files
sbox-public/game/addons/tools/Code/Widgets/SoundPlayer/SoundPlayer.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

278 lines
6.3 KiB
C#

namespace Editor;
public partial class SoundPlayer : Widget
{
private readonly TimelineView Timeline;
public bool Playing { get; set; }
public bool Repeating { get; set; }
public float Time { get; private set; }
public ToolBar ToolBar { get; private set; }
private readonly Option PlayOption;
private bool _prevPlay = false;
public SoundPlayer( Widget parent ) : base( parent )
{
Name = "Timeline";
WindowTitle = "Timeline";
SetWindowIcon( "timeline" );
Layout = Layout.Column();
var header = Layout.AddRow();
ToolBar = header.Add( new ToolBar( this ) );
ToolBar.NoSystemBackground = false;
ToolBar.SetIconSize( 18 );
PlayOption = ToolBar.AddOption( "Play", "play_arrow", () => Playing = !Playing );
var timecode = header.Add( new Label( this ) );
timecode.Bind( "Text" ).ReadOnly().From( () =>
{
TimeSpan t = TimeSpan.FromSeconds( Timeline.Time );
TimeSpan d = TimeSpan.FromSeconds( Timeline.Duration );
return $"{t.ToString( @"mm\:ss\.fff" )} / {d.ToString( @"mm\:ss\.fff" )}";
}, null );
timecode.Alignment = TextFlag.RightCenter;
var skipStart = ToolBar.AddOption( "Skip to Start", "skip_previous", () => Timeline.MoveScrubber( 0 ) );
ToolBar.AddSeparator();
var loop = ToolBar.AddOption( "Loop", "repeat" );
loop.Bind( "Checked" ).From( this, nameof( Repeating ) );
loop.Checkable = true;
Timeline = Layout.Add( new TimelineView( this ), 1 );
}
public void Play()
{
Playing = true;
}
public void Play( float start )
{
Timeline.MoveScrubber( start );
Play();
}
public void SetSamples( short[] samples, float duration, string sound )
{
Timeline.SetSamples( samples, duration, sound );
}
protected override void OnPaint()
{
base.OnPaint();
Paint.Antialiasing = false;
Paint.ClearPen();
Paint.SetBrush( Theme.WidgetBackground );
Paint.DrawRect( LocalRect );
}
[EditorEvent.Frame]
protected void OnFrame()
{
Timeline.OnFrame();
Time = Timeline.Time;
PlayOption.Text = Playing ? "Pause" : "Play";
PlayOption.Icon = Playing ? "pause" : "play_arrow";
if ( Application.FocusWidget.IsValid() )
{
if ( Application.IsKeyDown( KeyCode.Space ) && Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Ctrl ) && !_prevPlay )
{
Play( 0 );
}
else if ( Application.IsKeyDown( KeyCode.Space ) && !_prevPlay )
{
Playing = !Playing;
}
_prevPlay = Application.IsKeyDown( KeyCode.Space );
}
}
public class TimelineView : GraphicsView
{
private readonly SoundPlayer Timeline;
private readonly TimeAxis TimeAxis;
private readonly Scrubber Scrubber;
private readonly WaveForm WaveForm;
public float ZoomLevel { get; set; } = 1.0f;
public float Duration { get; private set; }
public float Time { get; set; }
public bool Scrubbing { get; set; }
public string Sound { get; private set; }
public SoundHandle SoundHandle { get; private set; }
public TimelineView( SoundPlayer parent ) : base( parent )
{
Timeline = parent;
SceneRect = new( 0, Size );
HorizontalScrollbar = ScrollbarMode.On;
VerticalScrollbar = ScrollbarMode.Off;
Scale = 1;
Time = 0;
WaveForm = new WaveForm( this );
Add( WaveForm );
TimeAxis = new TimeAxis( this );
Add( TimeAxis );
Scrubber = new Scrubber( this );
Add( Scrubber );
DoLayout();
}
protected override void DoLayout()
{
base.DoLayout();
var size = Size;
size.x = MathF.Max( Size.x, PositionFromTime( Duration ) );
SceneRect = new( 0, size );
TimeAxis.Size = new Vector2( size.x, Theme.RowHeight );
Scrubber.Size = new Vector2( 9, size.y );
var r = SceneRect;
r.Position = Vector2.Zero.WithY( Theme.RowHeight );
r.Size = new Vector2( PositionFromTime( Duration ), Size.y - Theme.RowHeight );
WaveForm.SceneRect = r;
WaveForm.Analyse();
Scrubber.Position = Scrubber.Position.WithX( PositionFromTime( Time ) - 3 ).SnapToGrid( 1.0f );
}
protected override void OnResize()
{
base.OnResize();
DoLayout();
}
public override void OnDestroyed()
{
base.OnDestroyed();
SoundHandle?.Stop( 0.0f );
SoundHandle = null;
}
public Rect VisibleRect { get; private set; }
public void OnFrame()
{
Time = Time.Clamp( 0, Duration );
if ( !Timeline.Playing )
{
SoundHandle?.Stop( 0.0f );
SoundHandle = null;
}
VisibleRect = Rect.FromPoints( ToScene( LocalRect.TopLeft ), ToScene( LocalRect.BottomRight ) );
if ( Timeline.Playing && !Scrubbing )
{
Time += RealTime.Delta;
var time = Time % Duration;
if ( time < Time )
{
if ( Timeline.Repeating )
{
Time = time;
if ( SoundHandle.IsValid() )
{
SoundHandle.Time = Time;
}
}
else
{
time = 0;
Time = time;
SoundHandle?.Stop( 0.0f );
Timeline.Playing = false;
}
}
if ( Timeline.Playing && !SoundHandle.IsValid() )
{
SoundHandle = EditorUtility.PlaySound( Sound, Time );
SoundHandle.Occlusion = false;
SoundHandle.DistanceAttenuation = false;
}
Scrubber.Position = Scrubber.Position.WithX( PositionFromTime( Time ) - 3 ).SnapToGrid( 1.0f );
}
if ( SoundHandle.IsValid() )
{
SoundHandle.Paused = Scrubbing;
}
TimeAxis.Update();
WaveForm.Update();
if ( Scrubbing || Timeline.Playing )
{
CenterOn( Scrubber.Position );
}
}
public float PositionFromTime( float time )
{
return (time / Duration).Clamp( 0, 1 ) * (Width * ZoomLevel);
}
public float TimeFromPosition( float position )
{
return (position / (Width * ZoomLevel)).Clamp( 0, 1 ) * Duration;
}
public void SetSamples( short[] samples, float duration, string sound )
{
Sound = sound;
Duration = duration;
WaveForm.SetSamples( samples, duration );
}
public void MoveScrubber( float position, bool centreOn = true )
{
Scrubber.Position = Vector2.Right * (position - 4).SnapToGrid( 1.0f );
Time = TimeFromPosition( Scrubber.Position.x + 4 );
if ( SoundHandle.IsValid() )
{
SoundHandle.Time = Time;
}
if ( centreOn )
{
CenterOn( Scrubber.Position );
VisibleRect = Rect.FromPoints( ToScene( LocalRect.TopLeft ), ToScene( LocalRect.BottomRight ) );
}
WaveForm.Update();
}
protected override void OnWheel( WheelEvent e )
{
ZoomLevel *= e.Delta > 0 ? 1.1f : 0.90f;
ZoomLevel = ZoomLevel.Clamp( 1.0f, 20.0f );
DoLayout();
TimeAxis.Update();
WaveForm.Update();
e.Accept();
}
}
}