Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
94325ed
feat: implement element preview functionality with image and video su…
yuto-trd Dec 5, 2025
e803b68
feat: add audio waveform preview support with chunk handling
yuto-trd Dec 5, 2025
bd851f7
feat: remove image preview support and update waveform handling
yuto-trd Dec 5, 2025
1188891
feat: enhance video rendering with internal draw method and resource …
yuto-trd Dec 5, 2025
590198c
feat: implement preview invalidation mechanism for audio and video el…
yuto-trd Dec 5, 2025
766bc0b
feat: add speed and loop properties to video source configuration
yuto-trd Dec 5, 2025
b978258
feat: improve audio composition by integrating `Composer` and optimiz…
yuto-trd Dec 5, 2025
1aa7a81
feat: add toggle for disabling preview and manage preview state in ti…
yuto-trd Dec 5, 2025
796fd4c
feat: refactor thumbnail generation to improve performance and update…
yuto-trd Dec 5, 2025
29f0221
feat: update dispatcher reference in waveform chunk rendering for imp…
yuto-trd Dec 6, 2025
6f48eff
feat: enhance buffer validation in waveform chunk rendering to check …
yuto-trd Dec 6, 2025
4614b39
feat: add logging for preview update errors in ElementViewModel
yuto-trd Dec 6, 2025
ce8a3d5
feat: replace ConcurrentBag with ConcurrentDictionary for property re…
yuto-trd Dec 6, 2025
279c116
feat: rename preview-related identifiers to thumbnails for clarity an…
yuto-trd Dec 6, 2025
a127168
fix: remove redundant semicolon in Dispose method for cleaner code
yuto-trd Dec 6, 2025
8748769
Update src/Beutl.Operators/Source/SourceVideoOperator.cs
yuto-trd Dec 6, 2025
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
17 changes: 10 additions & 7 deletions src/Beutl.Engine.SourceGenerators/EngineObjectResourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
namespace Beutl.Engine.SourceGenerators;

[Generator]
public sealed class EngineObjectResourceGenerator : IIncrementalGenerator

Check warning on line 17 in src/Beutl.Engine.SourceGenerators/EngineObjectResourceGenerator.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

'Beutl.Engine.SourceGenerators.EngineObjectResourceGenerator': A project containing analyzers or source generators should specify the property '<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>'

Check warning on line 17 in src/Beutl.Engine.SourceGenerators/EngineObjectResourceGenerator.cs

View workflow job for this annotation

GitHub Actions / build

'Beutl.Engine.SourceGenerators.EngineObjectResourceGenerator': A project containing analyzers or source generators should specify the property '<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>'
{
private static readonly DiagnosticDescriptor s_missingPartialDiagnostic = new(
id: "BESG001",
Expand Down Expand Up @@ -551,24 +551,27 @@
sb.Append(innerIndent).AppendLine("protected override void Dispose(bool disposing)");
sb.Append(innerIndent).AppendLine("{");
sb.Append(innerIndent).AppendLine(" this.PreDispose(disposing);");
sb.Append(innerIndent).AppendLine(" if (disposing)");
sb.Append(innerIndent).AppendLine(" {");
foreach (ObjectPropertyInfo property in info.ObjectProperties)
{
string fieldName = ToFieldName(property.Name);
sb.Append(innerIndent).AppendLine($" {fieldName}?.Dispose();");
sb.Append(innerIndent).AppendLine($" {fieldName}?.Dispose();");
}

foreach (ListPropertyInfo property in info.ListProperties)
{
string fieldName = ToFieldName(property.Name);
sb.Append(innerIndent).AppendLine($" if ({fieldName} != null)");
sb.Append(innerIndent).AppendLine(" {");
sb.Append(innerIndent).AppendLine($" foreach (var item in {fieldName})");
sb.Append(innerIndent).AppendLine($" if ({fieldName} != null)");
sb.Append(innerIndent).AppendLine(" {");
sb.Append(innerIndent).AppendLine(" item?.Dispose();");
sb.Append(innerIndent).AppendLine($" foreach (var item in {fieldName})");
sb.Append(innerIndent).AppendLine(" {");
sb.Append(innerIndent).AppendLine(" item?.Dispose();");
sb.Append(innerIndent).AppendLine(" }");
sb.Append(innerIndent).AppendLine(" ");
sb.Append(innerIndent).AppendLine(" }");
sb.Append(innerIndent).AppendLine(" ");
sb.Append(innerIndent).AppendLine(" }");
}
sb.Append(innerIndent).AppendLine(" }");
sb.Append(innerIndent).AppendLine(" this.PostDispose(disposing);");
sb.Append(innerIndent).AppendLine(" base.Dispose(disposing);");
sb.Append(innerIndent).AppendLine("}");
Expand Down
7 changes: 2 additions & 5 deletions src/Beutl.Engine/Audio/Composing/Composer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ protected virtual void ComposeCore(TimeRange timeRange)
}
}

protected void AddSound(Sound sound)
public void AddSound(Sound sound)
{
_currentSounds.Add(sound);
}

protected void ClearSounds()
public void ClearSounds()
{
_currentSounds.Clear();
}
Expand All @@ -88,9 +88,6 @@ protected void ClearSounds()
{
IsAudioRendering = true;

// Clear previous sounds list
_currentSounds.Clear();

// Let subclass populate sounds
ComposeCore(timeRange);

Expand Down
10 changes: 5 additions & 5 deletions src/Beutl.Engine/Engine/EngineObject.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Reactive.Disposables;
using System.Reflection;
Expand Down Expand Up @@ -220,16 +221,15 @@ protected void ScanProperties<T>() where T : EngineObject
{
if (!typeof(IProperty).IsAssignableFrom(propertyInfo.PropertyType)) continue;

var func = ReflectionCache<T>.Properties.FirstOrDefault(x => x.Item1 == propertyInfo).Item2;
if (func == null)
if (!ReflectionCache<T>.Properties.TryGetValue(propertyInfo, out var func))
{
var param = LinqExpression.Parameter(typeof(object), "o");
var cast = LinqExpression.Convert(param, type);
var propertyAccess = LinqExpression.Property(cast, propertyInfo);
var convertResult = LinqExpression.Convert(propertyAccess, typeof(IProperty));
var lambda = LinqExpression.Lambda<Func<object, IProperty?>>(convertResult, param);
func = lambda.Compile();
ReflectionCache<T>.Properties.Add((propertyInfo, func));
ReflectionCache<T>.Properties[propertyInfo] = func;
}

var property = func(this);
Expand Down Expand Up @@ -451,7 +451,7 @@ public void Dispose()

private static class ReflectionCache<T>
{
public static readonly List<(PropertyInfo, Func<object, IProperty?>)> Properties = new();
public static readonly Dictionary<string, IValidator> Validators = new();
public static readonly ConcurrentDictionary<PropertyInfo, Func<object, IProperty?>> Properties = new();
public static readonly ConcurrentDictionary<string, IValidator> Validators = new();
}
}
5 changes: 5 additions & 0 deletions src/Beutl.Engine/Graphics/SourceVideo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ protected override void OnDraw(GraphicsContext2D context, Drawable.Resource reso
}
}

internal void DrawInternal(GraphicsContext2D context, Drawable.Resource resource)
{
OnDraw(context, resource);
}

public partial class Resource
{
public TimeSpan RenderedPosition { get; internal set; }
Expand Down
1 change: 1 addition & 0 deletions src/Beutl.Engine/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Beutl")]
[assembly: InternalsVisibleTo("Beutl.Operators")]
[assembly: InternalsVisibleTo("Beutl.Controls")]
[assembly: InternalsVisibleTo("Beutl.ProjectSystem")]
[assembly: InternalsVisibleTo("Beutl.UnitTests")]
Expand Down
14 changes: 10 additions & 4 deletions src/Beutl.Language/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Beutl.Language/Strings.ja.resx
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,9 @@
<data name="UseNode" xml:space="preserve">
<value>ノードを使う</value>
</data>
<data name="DisableThumbnails" xml:space="preserve">
<value>サムネイルを無効化</value>
</data>
<data name="Unsupported" xml:space="preserve">
<value>非対応</value>
</data>
Expand Down
3 changes: 3 additions & 0 deletions src/Beutl.Language/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,9 @@
<data name="UseNode" xml:space="preserve">
<value>UseNode</value>
</data>
<data name="DisableThumbnails" xml:space="preserve">
<value>Disable Thumbnails</value>
</data>
<data name="Unsupported" xml:space="preserve">
<value>Unsupported</value>
</data>
Expand Down
2 changes: 1 addition & 1 deletion src/Beutl.Operators/Source/SourceImageOperator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Beutl.Graphics;
using Beutl.Graphics;
using Beutl.Graphics.Effects;
using Beutl.Graphics.Transformation;
using Beutl.Media.Source;
Expand Down
111 changes: 106 additions & 5 deletions src/Beutl.Operators/Source/SourceSoundOperator.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
using Beutl.Audio;
using System.Runtime.CompilerServices;
using Beutl.Audio;
using Beutl.Audio.Composing;
using Beutl.Audio.Effects;
using Beutl.Graphics.Rendering;
using Beutl.Media;
using Beutl.Media.Source;
using Beutl.Operation;
using Beutl.Threading;

namespace Beutl.Operators.Source;

public sealed class SourceSoundOperator : PublishOperator<SourceSound>
public sealed class SourceSoundOperator : PublishOperator<SourceSound>, IElementThumbnailsProvider
{
private Uri? _uri;
private EventHandler? _handler;

public ElementThumbnailsKind ThumbnailsKind => ElementThumbnailsKind.Audio;

public event EventHandler? ThumbnailsInvalidated;

public override bool HasOriginalLength()
{
Expand All @@ -26,19 +36,31 @@ protected override void FillProperties()
protected override void OnDetachedFromHierarchy(in HierarchyAttachmentEventArgs args)
{
base.OnDetachedFromHierarchy(args);
if (Value is not { Source.CurrentValue: { Uri: { } uri } source } value) return;

if (Value is { } value && _handler != null)
{
value.Edited -= _handler;
}

_handler = null;

if (Value is not { Source.CurrentValue: { Uri: { } uri } source } v) return;

_uri = uri;
value.Source.CurrentValue = null;
v.Source.CurrentValue = null;
source.Dispose();
}

protected override void OnAttachedToHierarchy(in HierarchyAttachmentEventArgs args)
{
base.OnAttachedToHierarchy(args);
if (_uri is null) return;
if (Value is not { } value) return;

_handler = (_, _) => ThumbnailsInvalidated?.Invoke(this, EventArgs.Empty);
value.Edited += _handler;

if (_uri is null) return;

if (SoundSource.TryOpen(_uri, out SoundSource? source))
{
value.Source.CurrentValue = source;
Expand Down Expand Up @@ -78,4 +100,83 @@ public override bool TryGetOriginalLength(out TimeSpan timeSpan)
return base.OnSplit(backward, startDelta, lengthDelta);
}
}

public async IAsyncEnumerable<(int Index, int Count, IBitmap Thumbnail)> GetThumbnailStripAsync(
int maxWidth,
int maxHeight,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await Task.CompletedTask;
yield break;
}

public async IAsyncEnumerable<WaveformChunk> GetWaveformChunksAsync(
int chunkCount,
int samplesPerChunk,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (Value?.Source.CurrentValue is not { IsDisposed: false } source)
yield break;

if (chunkCount <= 0 || samplesPerChunk <= 0)
yield break;

var duration = source.Duration;
if (duration <= TimeSpan.Zero)
yield break;

int sampleRate = source.SampleRate;
int totalSamples = (int)(duration.TotalSeconds * sampleRate);

using var composer = new Composer { SampleRate = sampleRate };
composer.AddSound(Value);

for (int chunkIndex = 0; chunkIndex < chunkCount; chunkIndex++)
{
if (cancellationToken.IsCancellationRequested)
yield break;

int startSample = (int)((long)chunkIndex * totalSamples / chunkCount);
int endSample = (int)((long)(chunkIndex + 1) * totalSamples / chunkCount);
int sampleCount = Math.Min(endSample - startSample, samplesPerChunk);
TimeSpan startTime = Value.TimeRange.Start + TimeSpan.FromSeconds((double)startSample / sampleRate);
TimeSpan durationTime = TimeSpan.FromSeconds((double)sampleCount / sampleRate);

if (sampleCount <= 0)
continue;

var chunk = await ComposeThread.Dispatcher.InvokeAsync(() =>
{
if (cancellationToken.IsCancellationRequested)
return (WaveformChunk?)null;

using var buffer = composer.Compose(new TimeRange(startTime, durationTime));
if (buffer == null || buffer.SampleCount == 0)
return null;

var firstChannel = buffer.GetChannelData(0);
var secondChannel = buffer.GetChannelData(1);

float minValue = float.MaxValue;
float maxValue = float.MinValue;

for (int i = 0; i < buffer.SampleCount; i++)
{
float left = firstChannel[i];
float right = secondChannel[i];

float monoValue = (left + right) * 0.5f;
minValue = Math.Min(minValue, monoValue);
maxValue = Math.Max(maxValue, monoValue);
}

return new WaveformChunk(chunkIndex, minValue, maxValue);
}, DispatchPriority.Low, cancellationToken);

if (chunk.HasValue)
{
yield return chunk.Value;
}
}
}
}
Loading
Loading