DiamondMusicClip.cs
using System;

namespace Diamonds;

public enum DiamondMusicLayer { Pad, Bass, Drums, Harmony, Lead }

/// <summary>
/// One looping audio clip on the shared 150 BPM timeline. Its loop length is rounded to whole bars and it
/// plays aligned to the run's first bar, so any clip can swap in on a bar line.
/// </summary>
public record struct DiamondMusicClip()
{
	public SoundFile Sound { get; set; }
	public DiamondMusicLayer Layer { get; set; }
	/// <summary>Lowest intensity tier this clip plays in.</summary>
	[Range( 0, 3 )] public int MinTier { get; set; } = 0;
	/// <summary>Highest intensity tier this clip plays in.</summary>
	[Range( 0, 3 )] public int MaxTier { get; set; } = 3;
	/// <summary>Only plays in runs whose motif is listed here. Empty plays in every run.</summary>
	public List<string> Motifs { get; set; } = new();
	/// <summary>Relative chance among the clips eligible for this layer. Zero never plays.</summary>
	[Range( 0, 10 )] public float Weight { get; set; } = 1;
	/// <summary>Level trim against the other clips, in decibels. Zero plays the file as rendered.</summary>
	[Title( "Volume (dB)" ), Range( -24, 12 )] public float VolumeDb { get; set; }

	/// <summary>Linear gain for <see cref="VolumeDb"/>.</summary>
	public readonly float Gain() => VolumeDb == 0 ? 1 : MathF.Pow( 10, VolumeDb / 20 );

	public readonly bool Fits( int tier, string motif )
	{
		if ( tier < MinTier || tier > MaxTier || Weight <= 0 ) return false;
		if ( Motifs is null || Motifs.Count == 0 ) return true;
		foreach ( var tag in Motifs )
			if ( string.Equals( tag?.Trim(), motif, StringComparison.OrdinalIgnoreCase ) ) return true;
		return false;
	}
}