Skip to content

Commit

Permalink
feat: waveform visualizer!
Browse files Browse the repository at this point in the history
  • Loading branch information
Lulalaby committed Aug 29, 2024
1 parent 85fcf54 commit 76b1a7b
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 2 deletions.
3 changes: 3 additions & 0 deletions DisCatSharp.Common/DisCatSharp.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
<PackageReference Include="DisCatSharp.Attributes" Version="10.6.6" />
<PackageReference Include="Microsoft.DependencyValidation.Analyzers" Version="0.11.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Maui.Graphics" Version="8.0.82" />
<PackageReference Include="Microsoft.Maui.Graphics.Skia" Version="8.0.82" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.Memory" Version="4.5.5" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
Expand Down
38 changes: 38 additions & 0 deletions DisCatSharp.Common/Utilities/WaveformConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;

using Newtonsoft.Json;

namespace DisCatSharp.Common.Utilities;

/// <inheritdoc />
public sealed class WaveformConverter : JsonConverter<byte[]?>
{
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, byte[]? value, JsonSerializer serializer)
{
if (value is not null)
writer.WriteValue(Convert.ToBase64String(value));
else
writer.WriteNull();
}

/// <inheritdoc />
public override byte[]? ReadJson(JsonReader reader, Type objectType, byte[]? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType is not JsonToken.String)
return existingValue;

var base64String = (string?)reader.Value;
if (base64String is null)
return null;

try
{
return Convert.FromBase64String(base64String);
}
catch (FormatException)
{
return existingValue;
}
}
}
176 changes: 176 additions & 0 deletions DisCatSharp.Common/Utilities/WaveformVisualizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Skia;

namespace DisCatSharp.Common.Utilities;

/// <summary>
/// Provides a <see cref="WaveformVisualizer" />.
/// </summary>
public sealed class WaveformVisualizer
{
/// <summary>
/// Gets the image.
/// </summary>
public IImage? Image { get; internal set; }

/// <summary>
/// Gets or sets the waveform byte data.
/// </summary>
private byte[]? WAVEFORM_BYTE_DATA { get; set; }

/// <summary>
/// Decodes the base 64 encoded waveform.
/// </summary>
/// <param name="base64Waveform">The base64 encoded waveform string.</param>
/// <returns></returns>
private static byte[] DecodeWaveform(string base64Waveform)
=> Convert.FromBase64String(base64Waveform);

/// <summary>
/// Attaches the raw waveform data as <see cref="byte" /> array.
/// </summary>
/// <param name="waveformByteData">The waveforms byte array.</param>
public WaveformVisualizer WithWaveformByteData(byte[] waveformByteData)
{
this.WAVEFORM_BYTE_DATA = waveformByteData;
return this;
}

/// <summary>
/// Attaches the raw waveform data as <see cref="byte" /> array.
/// </summary>
/// <param name="base64Waveform">The waveforms byte array encoded as base64.</param>
public WaveformVisualizer WithWaveformData(string base64Waveform)
{
this.WAVEFORM_BYTE_DATA = DecodeWaveform(base64Waveform);
return this;
}

/// <summary>
/// Creates a simple waveform image.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
public WaveformVisualizer CreateWaveformImage(int width = 500, int height = 100)
{
ArgumentNullException.ThrowIfNull(this.WAVEFORM_BYTE_DATA, nameof(this.WithWaveformByteData));

if (this.WAVEFORM_BYTE_DATA.Length is 0)
throw new ArgumentException("Waveform data is empty.", nameof(this.WithWaveformByteData));

var backgroundColor = Colors.Black;
Color[] barColors = [Colors.LightBlue, Colors.LightSkyBlue, Colors.DeepSkyBlue];

var image = new PlatformBitmapExportService().CreateContext(width, height);
var canvas = image.Canvas;
canvas.FillColor = backgroundColor;
canvas.FillRectangle(0, 0, width, height);

var barWidth = (float)width / this.WAVEFORM_BYTE_DATA.Length / 2;
var xScale = (float)width / this.WAVEFORM_BYTE_DATA.Length;
var yScale = (float)height / 2 / 255;

var gradientStops = barColors
.Select((color, index) => new PaintGradientStop((float)index / (barColors.Length - 1), color))
.ToArray();

var gradient = new LinearGradientPaint(gradientStops, new PointF(0, 0), new PointF(0, height));
canvas.SetFillPaint(gradient, new(0, 0, width, height));

for (var i = 0; i < this.WAVEFORM_BYTE_DATA.Length; i++)
{
var x1 = i * xScale;
var barHeight = Math.Max(2, this.WAVEFORM_BYTE_DATA[i] * yScale);
var y1 = ((float)height / 2) - barHeight;
var y2 = ((float)height / 2) + barHeight;

canvas.FillRoundedRectangle(x1, y1, barWidth, barHeight * 2, barWidth / 4);

canvas.SetShadow(new(2, 2), 5, Colors.Black.WithAlpha(0.5f));
}

this.Image = image.Image;

return this;
}

/// <summary>
/// Creates a colorful waveform image.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <param name="backgroundColor">The background color.</param>
/// <param name="barColors">The bar colors.</param>
public WaveformVisualizer CreateColorfulWaveformImage(int width = 500, int height = 100, Color? backgroundColor = null, params Color[]? barColors)
{
ArgumentNullException.ThrowIfNull(this.WAVEFORM_BYTE_DATA, nameof(this.WithWaveformByteData));

if (this.WAVEFORM_BYTE_DATA.Length is 0)
throw new ArgumentException("Waveform data is empty.", nameof(this.WithWaveformByteData));

backgroundColor ??= Colors.Black;
barColors ??= [Colors.DarkOrange, Colors.Orange, Colors.Gold, Colors.Yellow, Colors.LightBlue, Colors.Blue, Colors.BlueViolet, Colors.Pink, Colors.DeepPink, Colors.MediumVioletRed, Colors.Red];

if (barColors.Length is 0)
barColors = [Colors.DarkOrange, Colors.Orange, Colors.Gold, Colors.Yellow, Colors.LightBlue, Colors.Blue, Colors.BlueViolet, Colors.Pink, Colors.DeepPink, Colors.MediumVioletRed, Colors.Red];

var image = new PlatformBitmapExportService().CreateContext(width, height);
var canvas = image.Canvas;
canvas.FillColor = backgroundColor;
canvas.FillRectangle(0, 0, width, height);

var barWidth = (float)width / this.WAVEFORM_BYTE_DATA.Length / 2;
var xScale = (float)width / this.WAVEFORM_BYTE_DATA.Length;
var yScale = (float)height / 2 / 255;

for (var i = 0; i < this.WAVEFORM_BYTE_DATA.Length; i++)
{
var x1 = i * xScale;
var barHeight = Math.Max(2, this.WAVEFORM_BYTE_DATA[i] * yScale);
var y1 = ((float)height / 2) - barHeight;
var y2 = ((float)height / 2) + barHeight;

var color1 = barColors[i % barColors.Length];
var color2 = barColors[(i + 1) % barColors.Length];

var gradientStops = new PaintGradientStop[] { new(0, color1), new(1, color2) };

var gradient = new LinearGradientPaint(gradientStops, new PointF(x1, y1), new PointF(x1, y2));
canvas.SetFillPaint(gradient, new(x1, y1, barWidth, barHeight * 2));

canvas.FillRoundedRectangle(x1, y1, barWidth, barHeight * 2, barWidth / 4);

canvas.SetShadow(new(2, 2), 5, Colors.Black.WithAlpha(0.5f));
}

this.Image = image.Image;

return this;
}

/// <summary>
/// Saves a waveform image to given <paramref name="filePath" />.
/// </summary>
/// <param name="filePath">The path, including the files name, to save the image to.</param>
public async Task<WaveformVisualizer> SaveImageAsync(string filePath)
{
ArgumentNullException.ThrowIfNull(this.Image, nameof(this.Image));
await using var stream = File.OpenWrite(filePath);
await this.Image.SaveAsync(stream);
return this;
}

/// <summary>
/// Converts the waveform image to a <see cref="Stream" />.
/// </summary>
/// <param name="format">The image format. Defaults to <see cref="ImageFormat.Png" />.</param>
public Stream AsStream(ImageFormat format = ImageFormat.Png)
=> this.Image is not null
? this.Image.AsStream(format)
: throw new NullReferenceException("Image was null, did you even generate a waveform image?");
}
20 changes: 18 additions & 2 deletions DisCatSharp/Entities/Message/DiscordAttachment.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using DisCatSharp.Common.Utilities;
using DisCatSharp.Enums;

using Newtonsoft.Json;
Expand Down Expand Up @@ -100,12 +101,27 @@ internal DiscordAttachment()
/// voice message.
/// </para>
/// </summary>
[JsonProperty("waveform", NullValueHandling = NullValueHandling.Ignore)]
public string WaveForm { get; internal set; }
[JsonProperty("waveform", NullValueHandling = NullValueHandling.Ignore), JsonConverter(typeof(WaveformConverter))]
public byte[]? WaveForm { get; internal set; }

/// <summary>
/// Gets the attachment flags.
/// </summary>
[JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)]
public AttachmentFlags Flags { get; internal set; } = AttachmentFlags.None;

/// <summary>
/// Visualizes the <see cref="WaveForm" /> as image.
/// </summary>
/// <param name="colorful">Whether to use a colorful image. Defaults to <see langword="true" />.</param>
/// <returns>
/// A waveform visualizer object, or <see langword="nulL" /> if <see cref="WaveForm" /> is <see langword="nulL" />
/// .
/// </returns>
public WaveformVisualizer? VisualizeWaveForm(bool colorful = true)
=> this.WaveForm is not null
? colorful
? new WaveformVisualizer().WithWaveformByteData(this.WaveForm).CreateColorfulWaveformImage()
: new WaveformVisualizer().WithWaveformByteData(this.WaveForm).CreateWaveformImage()
: null;
}

0 comments on commit 76b1a7b

Please sign in to comment.