@pro-gram/flux 0.0.0-stage → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/NOTICE.txt +33 -0
- package/README.md +125 -2
- package/bin/flux.js +29 -0
- package/dependencies.json +15 -0
- package/package.json +60 -6
- package/payload/DrumBuddy-Setup.exe +4 -0
- package/scripts/install.js +60 -0
- package/scripts/version.js +12 -0
- package/source/AssetBootstrap.cs +49 -0
- package/source/Audio Decoders.txt +230 -0
- package/source/Audio.cs +453 -0
- package/source/AudioDeviceSettings.cs +126 -0
- package/source/AudioEndpointWatcher.cs +25 -0
- package/source/AudioInputs.cs +50 -0
- package/source/AudioOutputs.cs +307 -0
- package/source/AudioWorkspace.cs +72 -0
- package/source/BrandArtwork.cs +71 -0
- package/source/Build Instructions.txt +65 -0
- package/source/Build.ps1 +135 -0
- package/source/ControlMode.cs +687 -0
- package/source/ControlTests.cs +88 -0
- package/source/ConversionFormats.cs +59 -0
- package/source/ConversionTests.cs +66 -0
- package/source/DockablePane.cs +95 -0
- package/source/DriverSetup.cs +28 -0
- package/source/EditorCompactLayout.cs +36 -0
- package/source/EditorPlaybackTests.cs +87 -0
- package/source/EditorSelectionTests.cs +73 -0
- package/source/EditorToolInteractions.cs +227 -0
- package/source/EditorTools.cs +78 -0
- package/source/EditorToolsTests.cs +93 -0
- package/source/EditorView.cs +798 -0
- package/source/Fetch-Dependencies.ps1 +15 -0
- package/source/FloatRecording.cs +99 -0
- package/source/FloatRecordingTests.cs +170 -0
- package/source/Flux-Logo.png +0 -0
- package/source/Flux-Mark.svg +49 -0
- package/source/FluxTempo Logo Prompt.txt +3 -0
- package/source/FluxTempo-Mark.svg +49 -0
- package/source/HomeView.cs +32 -0
- package/source/InterfaceSettings.cs +225 -0
- package/source/InterfaceTests.cs +172 -0
- package/source/LICENSE.txt +23 -0
- package/source/Logo Prompt.txt +16 -0
- package/source/Main.cs +787 -0
- package/source/Main.xaml +289 -0
- package/source/MediaPlayerView.cs +274 -0
- package/source/MidiControl.cs +113 -0
- package/source/MidiPerformance.cs +125 -0
- package/source/Mixer.cs +280 -0
- package/source/NativeMemory.cs +165 -0
- package/source/PanelFeedback.cs +75 -0
- package/source/PlaybackTests.cs +141 -0
- package/source/Quick Start.txt +96 -0
- package/source/SelfTest.cs +78 -0
- package/source/SettingsWindow.cs +209 -0
- package/source/Setup.cs +196 -0
- package/source/Setup.xaml +28 -0
- package/source/ShellStyles.xaml +99 -0
- package/source/TempoGrid.cs +95 -0
- package/source/TempoGridTests.cs +108 -0
- package/source/Third Party Notices.txt +33 -0
- package/source/Timeline.cs +268 -0
- package/source/TimelineGestures.cs +57 -0
- package/source/TimelineZoom.cs +21 -0
- package/source/Uninstall.cs +69 -0
- package/source/WaveformDetail.cs +104 -0
- package/source/WindowsAudioPlayer.cs +137 -0
- package/source/WorkspaceTests.cs +202 -0
- package/source/ZoomTests.cs +95 -0
- package/source/app.manifest +7 -0
- package/source/brand-archive/Drum-Buddy-Black-Hair.png +0 -0
- package/source/dependency-lock.json +62 -0
- package/source/fluxtempo-mark.png +0 -0
- package/source/logo.png +0 -0
- package/source/r16-panel.png +0 -0
- package/source/references/R16 Format Research.txt +16 -0
- package/test/package.test.js +55 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
param([string]$Dependencies = (Join-Path $PSScriptRoot '..\deps'))
|
|
2
|
+
$ErrorActionPreference = 'Stop'
|
|
3
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
4
|
+
$Dependencies = [IO.Path]::GetFullPath($Dependencies)
|
|
5
|
+
$downloads = Join-Path $Dependencies 'downloads'
|
|
6
|
+
New-Item -ItemType Directory -Force -Path $Dependencies,$downloads | Out-Null
|
|
7
|
+
$locked = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'dependency-lock.json') -Raw | ConvertFrom-Json
|
|
8
|
+
foreach ($item in $locked) {
|
|
9
|
+
$download = Join-Path $downloads $item.File
|
|
10
|
+
if (!(Test-Path -LiteralPath $download)) { Invoke-WebRequest -Uri $item.Url -OutFile $download }
|
|
11
|
+
if ((Get-FileHash -LiteralPath $download -Algorithm SHA256).Hash -ne $item.Sha256) { throw ('Dependency hash mismatch: ' + $item.File + '. The upstream file may have changed. Obtain the pinned version rather than disabling this check.') }
|
|
12
|
+
if ($item.ExtractTo) { Expand-Archive -LiteralPath $download -DestinationPath (Join-Path $Dependencies $item.ExtractTo) -Force }
|
|
13
|
+
else { Copy-Item -LiteralPath $download -Destination (Join-Path $Dependencies $item.File) -Force }
|
|
14
|
+
}
|
|
15
|
+
Write-Output ('Dependencies ready in ' + $Dependencies)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Text;
|
|
4
|
+
|
|
5
|
+
namespace DrumBuddy
|
|
6
|
+
{
|
|
7
|
+
public interface IRecordingWriter : IDisposable
|
|
8
|
+
{
|
|
9
|
+
void Write(float[] samples, int count, double gain);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// A recording layout is based on the channels actually exposed by the ASIO driver.
|
|
13
|
+
public sealed class CaptureLayout
|
|
14
|
+
{
|
|
15
|
+
public int FirstInput, Channels;
|
|
16
|
+
public bool Separate;
|
|
17
|
+
public static bool IsH5studio(string driver)
|
|
18
|
+
{ return driver != null && driver.Replace(" ", "").IndexOf("H5studio", StringComparison.OrdinalIgnoreCase) >= 0; }
|
|
19
|
+
public static CaptureLayout Create(int available, int mode)
|
|
20
|
+
{
|
|
21
|
+
if (available < 1) throw new IOException("The selected ASIO device has no recording inputs.");
|
|
22
|
+
var layout = new CaptureLayout { Separate = mode == -1, FirstInput = mode > 0 ? mode - 1 : 0 };
|
|
23
|
+
layout.Channels = layout.Separate ? Math.Min(8, available) : mode == 0 ? 2 : 1;
|
|
24
|
+
if (mode < -1 || layout.FirstInput < 0 || layout.FirstInput + layout.Channels > available || layout.FirstInput + layout.Channels > 8)
|
|
25
|
+
throw new IOException("This device does not have those inputs. Refresh the device and choose an available input.");
|
|
26
|
+
return layout;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// IEEE float WAV: fmt (tag 3), fact (frame count), then interleaved float data.
|
|
31
|
+
// Values above +1 or below -1 are kept exactly; only a later PCM export limits peaks.
|
|
32
|
+
public sealed class FloatWaveWriter : IRecordingWriter
|
|
33
|
+
{
|
|
34
|
+
public const long MaxData = 0xFFFFFF00L;
|
|
35
|
+
private readonly FileStream stream;
|
|
36
|
+
private readonly BinaryWriter writer;
|
|
37
|
+
private readonly int channels, rate;
|
|
38
|
+
private byte[] bytes = new byte[0];
|
|
39
|
+
private float[] scaled = new float[0];
|
|
40
|
+
private long dataSize;
|
|
41
|
+
private bool disposed;
|
|
42
|
+
|
|
43
|
+
public FloatWaveWriter(string path, int channels, int rate)
|
|
44
|
+
{
|
|
45
|
+
if (channels < 1 || channels > 8 || (rate != 44100 && rate != 48000 && rate != 96000))
|
|
46
|
+
throw new ArgumentException("Unsupported float WAV format.");
|
|
47
|
+
this.channels = channels; this.rate = rate;
|
|
48
|
+
stream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read, 65536);
|
|
49
|
+
writer = new BinaryWriter(stream);
|
|
50
|
+
writer.Write(Encoding.ASCII.GetBytes("RIFF")); writer.Write((uint)50);
|
|
51
|
+
writer.Write(Encoding.ASCII.GetBytes("WAVEfmt ")); writer.Write(18);
|
|
52
|
+
writer.Write((ushort)3); writer.Write((ushort)channels); writer.Write(rate);
|
|
53
|
+
writer.Write(rate * channels * 4); writer.Write((ushort)(channels * 4)); writer.Write((ushort)32); writer.Write((ushort)0);
|
|
54
|
+
writer.Write(Encoding.ASCII.GetBytes("fact")); writer.Write(4); writer.Write((uint)0);
|
|
55
|
+
writer.Write(Encoding.ASCII.GetBytes("data")); writer.Write((uint)0);
|
|
56
|
+
stream.Flush();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public void Write(float[] samples, int count, double gain)
|
|
60
|
+
{
|
|
61
|
+
if (disposed) throw new ObjectDisposedException("FloatWaveWriter");
|
|
62
|
+
if (samples == null || count < 0 || count > samples.Length || count % channels != 0)
|
|
63
|
+
throw new ArgumentException("Incomplete audio frame.");
|
|
64
|
+
long size = (long)count * 4;
|
|
65
|
+
if (dataSize + size > MaxData)
|
|
66
|
+
throw new IOException("This WAV take has reached its 4 GB limit. Stop and start a new take. The audio already saved is kept.");
|
|
67
|
+
if (double.IsNaN(gain) || double.IsInfinity(gain)) throw new ArgumentException("Invalid audio gain.");
|
|
68
|
+
for (int i = 0; i < count; i++)
|
|
69
|
+
if (float.IsNaN(samples[i]) || float.IsInfinity(samples[i]) || Math.Abs((double)samples[i] * gain) > float.MaxValue)
|
|
70
|
+
throw new IOException("The driver sent an invalid audio sample. The audio already saved is kept.");
|
|
71
|
+
int byteCount = checked(count * 4);
|
|
72
|
+
if (bytes.Length < byteCount) bytes = new byte[byteCount];
|
|
73
|
+
if (gain == 1) Buffer.BlockCopy(samples, 0, bytes, 0, byteCount);
|
|
74
|
+
else {
|
|
75
|
+
if (scaled.Length < count) scaled = new float[count];
|
|
76
|
+
for (int i = 0; i < count; i++) scaled[i] = (float)(samples[i] * gain);
|
|
77
|
+
Buffer.BlockCopy(scaled, 0, bytes, 0, byteCount);
|
|
78
|
+
}
|
|
79
|
+
long before = dataSize;
|
|
80
|
+
stream.Write(bytes, 0, byteCount); dataSize += byteCount;
|
|
81
|
+
if (dataSize / (rate * channels * 4) != before / (rate * channels * 4)) UpdateHeader();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private void UpdateHeader()
|
|
85
|
+
{
|
|
86
|
+
long position = stream.Position;
|
|
87
|
+
stream.Position = 4; writer.Write((uint)(50 + dataSize));
|
|
88
|
+
stream.Position = 46; writer.Write((uint)(dataSize / (channels * 4)));
|
|
89
|
+
stream.Position = 54; writer.Write((uint)dataSize);
|
|
90
|
+
stream.Position = position; stream.Flush();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public void Dispose()
|
|
94
|
+
{
|
|
95
|
+
if (disposed) return; disposed = true;
|
|
96
|
+
try { UpdateHeader(); } finally { writer.Dispose(); }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Text;
|
|
5
|
+
using System.Threading;
|
|
6
|
+
using System.Collections.Generic;
|
|
7
|
+
using System.Runtime.InteropServices;
|
|
8
|
+
using NAudio.Wave;
|
|
9
|
+
using NAudio.Wave.Asio;
|
|
10
|
+
|
|
11
|
+
namespace DrumBuddy
|
|
12
|
+
{
|
|
13
|
+
public static class FloatRecordingTests
|
|
14
|
+
{
|
|
15
|
+
private static readonly List<string> results = new List<string>();
|
|
16
|
+
private static void Check(bool ok, string description)
|
|
17
|
+
{ if (!ok) throw new Exception(description); results.Add("PASS " + description); }
|
|
18
|
+
|
|
19
|
+
private static void Feed(Recorder recorder, float[][] channels)
|
|
20
|
+
{
|
|
21
|
+
var pointers = new IntPtr[channels.Length];
|
|
22
|
+
try {
|
|
23
|
+
for (int c = 0; c < channels.Length; c++) {
|
|
24
|
+
pointers[c] = Marshal.AllocHGlobal(channels[c].Length * 4);
|
|
25
|
+
Marshal.Copy(channels[c], 0, pointers[c], channels[c].Length);
|
|
26
|
+
}
|
|
27
|
+
recorder.ProcessCapture(new AsioAudioAvailableEventArgs(pointers, new IntPtr[0], channels[0].Length, AsioSampleType.Float32LSB));
|
|
28
|
+
}
|
|
29
|
+
finally { foreach (var pointer in pointers) if (pointer != IntPtr.Zero) Marshal.FreeHGlobal(pointer); }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private static float[] ReadFloat(string path, int rate, int channels)
|
|
33
|
+
{
|
|
34
|
+
using (var reader = new WaveFileReader(path)) {
|
|
35
|
+
Check(reader.WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat && reader.WaveFormat.BitsPerSample == 32,
|
|
36
|
+
"Independent WAV reader identifies IEEE float, rather than integer PCM");
|
|
37
|
+
Check(reader.WaveFormat.SampleRate == rate && reader.WaveFormat.Channels == channels,
|
|
38
|
+
"WAV retains " + rate + " Hz and " + channels + " channel(s)");
|
|
39
|
+
var bytes = new byte[checked((int)reader.Length)];
|
|
40
|
+
int read = 0, next;
|
|
41
|
+
while ((next = reader.Read(bytes, read, bytes.Length - read)) > 0) read += next;
|
|
42
|
+
var samples = new float[read / 4]; Buffer.BlockCopy(bytes, 0, samples, 0, read); return samples;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public static int Run(string[] args)
|
|
47
|
+
{
|
|
48
|
+
string root = Path.GetFullPath(args.Length > 0 ? args[0] : Path.Combine(Path.GetTempPath(), "DrumBuddyFloat_" + Guid.NewGuid().ToString("N")));
|
|
49
|
+
Directory.CreateDirectory(root); results.Clear();
|
|
50
|
+
try {
|
|
51
|
+
Check(CaptureLayout.IsH5studio("ZOOM H5studio ASIO Driver") && CaptureLayout.IsH5studio("ZOOM H5 Studio") && !CaptureLayout.IsH5studio("ZOOM H5 ASIO Driver"),
|
|
52
|
+
"Apply native float defaults to H5studio without confusing it with the older H5");
|
|
53
|
+
var pcmInput = new short[] { short.MinValue, 16384, -16384, short.MaxValue, 0, -8192, 8192, 0 };
|
|
54
|
+
var pcmBytes = new byte[pcmInput.Length * 2]; Buffer.BlockCopy(pcmInput, 0, pcmBytes, 0, pcmBytes.Length);
|
|
55
|
+
foreach (int mode in new[] { -1, 0, 2 }) {
|
|
56
|
+
var recorder = new Recorder(); var layout = CaptureLayout.Create(2, mode);
|
|
57
|
+
recorder.StartBuffered(layout, 32, Path.Combine(root, "windows-microphone-" + mode), 48000, false);
|
|
58
|
+
recorder.ProcessWindowsPcm(pcmBytes, pcmBytes.Length, 2); recorder.ProcessWindowsPcm(pcmBytes, pcmBytes.Length, 2); recorder.Stop();
|
|
59
|
+
recorder.ProcessWindowsPcm(pcmBytes, pcmBytes.Length, 2); recorder.Finish().GetAwaiter().GetResult();
|
|
60
|
+
Check(recorder.Error == null && recorder.Frames == 8 && !recorder.SourceIsFloat && recorder.SourceFormat == "Windows 16-bit PCM",
|
|
61
|
+
"Drain Windows PCM microphone buffers on Stop without claiming native float capture (mode " + mode + ")");
|
|
62
|
+
for (int file = 0; file < recorder.Paths.Count; file++) {
|
|
63
|
+
int savedChannels = layout.Separate ? 1 : layout.Channels;
|
|
64
|
+
var actual = ReadFloat(recorder.Paths[file], 48000, savedChannels); bool exact = actual.Length == 8 * savedChannels;
|
|
65
|
+
for (int f = 0; f < 8 && exact; f++) for (int c = 0; c < savedChannels; c++) {
|
|
66
|
+
int sourceChannel = layout.Separate ? file : layout.FirstInput + c;
|
|
67
|
+
exact &= actual[f * savedChannels + c] == pcmInput[(f % 4) * 2 + sourceChannel] / 32768f;
|
|
68
|
+
}
|
|
69
|
+
Check(exact, "Save exact Windows microphone samples with correct selected/separate channel routing (mode " + mode + ", file " + file + ")");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
var malformed = new Recorder(); malformed.StartBuffered(CaptureLayout.Create(1, 1), 16, Path.Combine(root, "windows-incomplete"), 44100, false);
|
|
73
|
+
malformed.ProcessWindowsPcm(new byte[3], 3, 1); malformed.Stop(); malformed.Finish().GetAwaiter().GetResult();
|
|
74
|
+
Check(malformed.Error is IOException && malformed.Frames == 0, "Reject incomplete Windows microphone frames without writing corrupt samples");
|
|
75
|
+
foreach (int channels in new[] { 2, 4 }) {
|
|
76
|
+
int rate = channels == 2 ? 48000 : 96000;
|
|
77
|
+
string folder = Path.Combine(root, channels + "-input-float");
|
|
78
|
+
var input = new float[channels][];
|
|
79
|
+
for (int c = 0; c < channels; c++) {
|
|
80
|
+
input[c] = new float[1024];
|
|
81
|
+
for (int i = 0; i < input[c].Length; i++) input[c][i] = (float)((c + 2.5) * Math.Sin(i * .071 + c * .3));
|
|
82
|
+
input[c][0] = c % 2 == 0 ? 4.25f + c : -3.5f - c;
|
|
83
|
+
}
|
|
84
|
+
var recorder = new Recorder();
|
|
85
|
+
recorder.StartBuffered(CaptureLayout.Create(channels, channels == 2 ? 0 : -1), 32, folder, rate, true);
|
|
86
|
+
Feed(recorder, input); Feed(recorder, input); recorder.Stop();
|
|
87
|
+
Feed(recorder, input); // A final driver callback after Stop must not append another block.
|
|
88
|
+
recorder.Finish().GetAwaiter().GetResult();
|
|
89
|
+
Check(recorder.Error == null && recorder.Frames == 2048 && recorder.SourceIsFloat,
|
|
90
|
+
"Drain queued ASIO float buffers on Stop and ignore late callbacks (" + channels + " inputs)");
|
|
91
|
+
Check(recorder.Paths.Count == (channels == 2 ? 1 : 4),
|
|
92
|
+
"Use the device's actual channel count for stereo / separate multitrack capture");
|
|
93
|
+
for (int file = 0; file < recorder.Paths.Count; file++) {
|
|
94
|
+
int fileChannels = channels == 2 ? 2 : 1;
|
|
95
|
+
var actual = ReadFloat(recorder.Paths[file], rate, fileChannels);
|
|
96
|
+
bool exact = actual.Length == 2048 * fileChannels;
|
|
97
|
+
for (int f = 0; f < 2048 && exact; f++)
|
|
98
|
+
for (int c = 0; c < fileChannels; c++)
|
|
99
|
+
exact &= actual[f * fileChannels + c] == input[fileChannels == 2 ? c : file][f % 1024];
|
|
100
|
+
Check(exact && actual.Any(value => Math.Abs(value) > 1),
|
|
101
|
+
"Preserve every float sample and headroom above 0 dBFS in " + Path.GetFileName(recorder.Paths[file]));
|
|
102
|
+
byte[] bytes = File.ReadAllBytes(recorder.Paths[file]);
|
|
103
|
+
Check(BitConverter.ToUInt32(bytes, 4) == bytes.Length - 8 && BitConverter.ToUInt16(bytes, 20) == 3 &&
|
|
104
|
+
Encoding.ASCII.GetString(bytes, 38, 4) == "fact" && BitConverter.ToUInt32(bytes, 46) == 2048 &&
|
|
105
|
+
BitConverter.ToUInt32(bytes, 54) == bytes.Length - 58,
|
|
106
|
+
"Finalize RIFF, IEEE float, fact-frame and data sizes correctly");
|
|
107
|
+
Audio.Run("-hide_banner -v error -nostdin -i " + Audio.Q(recorder.Paths[file]) + " -f null -", CancellationToken.None, null, true);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
var integerInput = new Recorder();
|
|
112
|
+
integerInput.StartBuffered(CaptureLayout.Create(2, 0), 32, Path.Combine(root, "integer-driver-rejected"), 48000, true);
|
|
113
|
+
IntPtr data = Marshal.AllocHGlobal(16);
|
|
114
|
+
try {
|
|
115
|
+
Marshal.Copy(new int[] { 0, int.MaxValue, int.MinValue, 0 }, 0, data, 4);
|
|
116
|
+
integerInput.ProcessCapture(new AsioAudioAvailableEventArgs(new[] { data, data }, new IntPtr[0], 4, AsioSampleType.Int32LSB));
|
|
117
|
+
}
|
|
118
|
+
finally { Marshal.FreeHGlobal(data); }
|
|
119
|
+
integerInput.Stop(); integerInput.Finish().GetAwaiter().GetResult();
|
|
120
|
+
Check(integerInput.Error != null && integerInput.Frames == 0 && integerInput.Error.Message.Contains("native 32-bit float"),
|
|
121
|
+
"Reject integer ASIO input when native H5studio float is required");
|
|
122
|
+
|
|
123
|
+
var invalid = new Recorder();
|
|
124
|
+
invalid.StartBuffered(CaptureLayout.Create(1, 1), 32, Path.Combine(root, "invalid-after-good"), 44100, true);
|
|
125
|
+
Feed(invalid, new[] { new[] { 2.5f, -2.25f, .125f } });
|
|
126
|
+
Feed(invalid, new[] { new[] { float.NaN } });
|
|
127
|
+
invalid.Stop(); invalid.Finish().GetAwaiter().GetResult();
|
|
128
|
+
Check(invalid.Error != null && ReadFloat(invalid.Paths[0], 44100, 1).SequenceEqual(new[] { 2.5f, -2.25f, .125f }),
|
|
129
|
+
"Keep valid captured audio when a later driver buffer contains NaN");
|
|
130
|
+
bool rejected = false;
|
|
131
|
+
try { CaptureLayout.Create(4, 5); } catch (IOException) { rejected = true; }
|
|
132
|
+
Check(rejected, "Reject nonexistent input 5 on a four-input H5studio");
|
|
133
|
+
|
|
134
|
+
foreach (int bits in new[] { 16, 24 }) {
|
|
135
|
+
var legacy = new Recorder();
|
|
136
|
+
legacy.StartBuffered(CaptureLayout.Create(8, -1), bits, Path.Combine(root, "r16-" + bits), 44100, false);
|
|
137
|
+
Feed(legacy, Enumerable.Range(0, 8).Select(c => new[] { c / 16.0f, -.25f }).ToArray());
|
|
138
|
+
legacy.Stop(); legacy.Finish().GetAwaiter().GetResult();
|
|
139
|
+
Check(legacy.Error == null && legacy.Paths.Count == 8, "Keep R16 eight-input " + bits + "-bit recording");
|
|
140
|
+
using (var wav = new WaveFileReader(legacy.Paths[7]))
|
|
141
|
+
Check(wav.WaveFormat.Encoding == WaveFormatEncoding.Pcm && wav.WaveFormat.BitsPerSample == bits && wav.WaveFormat.SampleRate == 44100 && wav.Length == 2 * bits / 8,
|
|
142
|
+
"Keep R16 classic PCM format and frame count");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
string master = Path.Combine(root, "2-input-float", "STEREO.WAV");
|
|
146
|
+
byte[] original = File.ReadAllBytes(master);
|
|
147
|
+
Clip clip = Audio.Load(master, Path.Combine(root, "decoded"), CancellationToken.None, null);
|
|
148
|
+
Check(clip.Ready && clip.Peak > 1, "Import a 48 kHz float WAV without discarding its over-0-dBFS samples");
|
|
149
|
+
clip.Gain = -1 - clip.PeakDb;
|
|
150
|
+
foreach (int bits in new[] { 16, 24 }) {
|
|
151
|
+
string path = Audio.Export(clip, Path.Combine(root, "compatible-" + bits), bits, true, CancellationToken.None, null);
|
|
152
|
+
var pcm = Audio.Load(path, Path.Combine(root, "decoded"), CancellationToken.None, null);
|
|
153
|
+
using (var wav = new WaveFileReader(path))
|
|
154
|
+
Check(wav.WaveFormat.Encoding == WaveFormatEncoding.Pcm && wav.WaveFormat.SampleRate == 44100 && wav.WaveFormat.BitsPerSample == bits,
|
|
155
|
+
"Make a standard " + bits + "-bit PCM playback copy from the float master");
|
|
156
|
+
Check(Math.Abs(pcm.PeakDb + 1) < .02, "Recover float peaks by reducing gain before PCM export");
|
|
157
|
+
}
|
|
158
|
+
Check(original.SequenceEqual(File.ReadAllBytes(master)), "Keep the original 32-bit float master byte-for-byte unchanged during conversion");
|
|
159
|
+
bool overwriteRejected = false;
|
|
160
|
+
try { using (var writer = new FloatWaveWriter(master, 2, 48000)) { } } catch (IOException) { overwriteRejected = true; }
|
|
161
|
+
Check(overwriteRejected && original.SequenceEqual(File.ReadAllBytes(master)), "Refuse to overwrite an existing master recording");
|
|
162
|
+
results.Add("Hardware acceptance pending: connect a physical H5studio with Zoom's Windows ASIO driver and test both Stereo mix and Multi Track.");
|
|
163
|
+
File.WriteAllLines(Path.Combine(root, "float-test-results.txt"), results, Encoding.UTF8); return 0;
|
|
164
|
+
}
|
|
165
|
+
catch (Exception ex) {
|
|
166
|
+
results.Add("FAIL " + ex); File.WriteAllLines(Path.Combine(root, "float-test-results.txt"), results, Encoding.UTF8); return 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="100 280 1050 800" role="img" aria-labelledby="title desc">
|
|
3
|
+
<title id="title">Flux mark</title>
|
|
4
|
+
<desc id="desc">A cyan-to-violet F with a continuous audio waveform, a bright icy-cyan crest and three tempo bars. Vector paths only.</desc>
|
|
5
|
+
<defs>
|
|
6
|
+
<linearGradient xmlns="http://www.w3.org/2000/svg" id="body" gradientUnits="userSpaceOnUse" x1="310" y1="1020" x2="780" y2="485">
|
|
7
|
+
<stop offset="0" stop-color="#00D7DD" />
|
|
8
|
+
<stop offset="0.36" stop-color="#047FBF" />
|
|
9
|
+
<stop offset="0.72" stop-color="#064397" />
|
|
10
|
+
<stop offset="1" stop-color="#022966" />
|
|
11
|
+
</linearGradient>
|
|
12
|
+
<linearGradient xmlns="http://www.w3.org/2000/svg" id="top" gradientUnits="userSpaceOnUse" x1="420" y1="360" x2="1130" y2="420">
|
|
13
|
+
<stop offset="0" stop-color="#00FFE0" />
|
|
14
|
+
<stop offset="0.48" stop-color="#009BFF" />
|
|
15
|
+
<stop offset="1" stop-color="#9426FF" />
|
|
16
|
+
</linearGradient>
|
|
17
|
+
<linearGradient xmlns="http://www.w3.org/2000/svg" id="lower" gradientUnits="userSpaceOnUse" x1="290" y1="1030" x2="830" y2="750">
|
|
18
|
+
<stop offset="0" stop-color="#00F2DC" />
|
|
19
|
+
<stop offset="0.35" stop-color="#00C8F2" />
|
|
20
|
+
<stop offset="0.64" stop-color="#146CFF" />
|
|
21
|
+
<stop offset="1" stop-color="#9527FF" />
|
|
22
|
+
</linearGradient>
|
|
23
|
+
<linearGradient xmlns="http://www.w3.org/2000/svg" id="wave" gradientUnits="userSpaceOnUse" x1="310" y1="650" x2="1000" y2="535">
|
|
24
|
+
<stop offset="0" stop-color="#00FFE1" />
|
|
25
|
+
<stop offset="0.3" stop-color="#40F9FF" />
|
|
26
|
+
<stop offset="0.36" stop-color="#ACFFFF" />
|
|
27
|
+
<stop offset="0.52" stop-color="#27DBFF" />
|
|
28
|
+
<stop offset="0.78" stop-color="#628EFF" />
|
|
29
|
+
<stop offset="1" stop-color="#BE31FF" />
|
|
30
|
+
</linearGradient>
|
|
31
|
+
<linearGradient xmlns="http://www.w3.org/2000/svg" id="tempo" gradientUnits="userSpaceOnUse" x1="770" y1="650" x2="990" y2="510">
|
|
32
|
+
<stop offset="0" stop-color="#28C8FF" />
|
|
33
|
+
<stop offset="0.5" stop-color="#8E8AFF" />
|
|
34
|
+
<stop offset="1" stop-color="#BF27FF" />
|
|
35
|
+
</linearGradient>
|
|
36
|
+
</defs>
|
|
37
|
+
<path fill="url(#body)" d="M260 1058 L446 545 C455 517 470 493 489 480 L466 459 L495 371 C514 319 552 294 608 294 H1118 C1139 294 1137 306 1123 330 L1097 374 C1079 407 1051 421 1012 421 H711 C676 421 661 432 649 465 L624 532 H903 C924 532 936 519 949 517 L983 516 C998 516 1002 525 994 540 L962 605 C947 637 922 652 885 655 L604 689 C576 695 556 711 540 738 H829 C847 738 850 744 841 760 L811 812 C798 835 776 844 745 844 H519 C489 844 475 861 462 890 L430 970 C408 1020 348 1052 260 1058 Z" />
|
|
38
|
+
<path fill="url(#top)" d="M371 579 L446 379 C466 326 514 294 575 294 H1123 C1144 294 1136 310 1123 330 L1097 374 C1079 407 1051 421 1012 421 H701 C639 421 602 421 557 444 C474 484 423 543 371 579 Z" />
|
|
39
|
+
<path fill="url(#lower)" d="M260 1058 C294 980 342 849 398 778 C437 725 481 693 532 692 C557 695 574 711 588 738 H829 C847 738 850 744 841 760 L811 812 C798 835 776 844 745 844 H519 C489 844 475 861 462 890 L430 970 C408 1020 348 1052 260 1058 Z" />
|
|
40
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="url(#wave)" d="M118 651 L318 642 C331 641 342 644 352 649 L352 659 C338 664 330 666 316 665 L118 660 C110 660 108 655 118 651 Z" />
|
|
41
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="none" stroke="url(#wave)" stroke-width="66" stroke-linecap="round" stroke-linejoin="round" opacity="0.06" d="M319 654 C351 654 363 628 385 599 C397 583 405 577 413 583 C427 596 440 641 456 669 C466 688 481 688 490 665 L533 514 C540 491 550 487 558 505 C571 535 573 604 584 635 C589 651 600 656 612 643 L646 610 C655 601 664 605 674 617 C689 635 708 644 725 637 C735 633 742 620 746 606 L760 568" />
|
|
42
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="none" stroke="url(#wave)" stroke-width="48" stroke-linecap="round" stroke-linejoin="round" opacity="0.14" d="M319 654 C351 654 363 628 385 599 C397 583 405 577 413 583 C427 596 440 641 456 669 C466 688 481 688 490 665 L533 514 C540 491 550 487 558 505 C571 535 573 604 584 635 C589 651 600 656 612 643 L646 610 C655 601 664 605 674 617 C689 635 708 644 725 637 C735 633 742 620 746 606 L760 568" />
|
|
43
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#052C69" stroke-width="36" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" d="M319 654 C351 654 363 628 385 599 C397 583 405 577 413 583 C427 596 440 641 456 669 C466 688 481 688 490 665 L533 514 C540 491 550 487 558 505 C571 535 573 604 584 635 C589 651 600 656 612 643 L646 610 C655 601 664 605 674 617 C689 635 708 644 725 637 C735 633 742 620 746 606 L760 568" />
|
|
44
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="none" stroke="url(#wave)" stroke-width="23" stroke-linecap="round" stroke-linejoin="round" opacity="1" d="M319 654 C351 654 363 628 385 599 C397 583 405 577 413 583 C427 596 440 641 456 669 C466 688 481 688 490 665 L533 514 C540 491 550 487 558 505 C571 535 573 604 584 635 C589 651 600 656 612 643 L646 610 C655 601 664 605 674 617 C689 635 708 644 725 637 C735 633 742 620 746 606 L760 568" />
|
|
45
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#D5FFFF" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.66" d="M509 603 L534 514 C541 492 550 490 558 508 C567 532 571 579 575 605" />
|
|
46
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="url(#tempo)" d="M777 639 C770 639 768 634 771 627 L805 549 C808 542 812 540 820 540 H839 C847 540 850 546 847 553 L815 631 C812 638 808 641 801 641 Z" />
|
|
47
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="url(#tempo)" d="M840 639 C833 639 831 634 834 627 L875 531 C878 524 883 522 891 522 H916 C924 522 926 528 923 535 L882 631 C879 638 875 641 867 641 Z" />
|
|
48
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="url(#tempo)" d="M904 639 C897 639 895 634 898 627 L941 526 C944 519 950 516 958 516 H984 C993 516 996 523 992 531 L952 624 C947 635 938 641 927 641 Z" />
|
|
49
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
Tool: built-in image_gen, logo-brand generation with transparent background.
|
|
2
|
+
Prompt: Create a polished standalone app emblem for FluxTempo, a sound production and multitrack recording/editor application. Short, clean, modern and energetic identity with a subtle sleek starship-instrument feel. A distinctive bold abstract F made from a flowing audio waveform and precisely spaced tempo-like strokes, forming one coherent simple symbol. Crisp smooth edges, electric cyan/teal to cool violet accents, strong small-icon silhouette, centered square composition, generous transparent padding. No text, wordmark, mascot, fantasy ornamentation, mockup or outer glow. Readable on charcoal and light surfaces.
|
|
3
|
+
The original black-haired Drum Buddy mascot is archived separately and preserved.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="100 280 1050 800" role="img" aria-labelledby="title desc">
|
|
3
|
+
<title id="title">FluxTempo mark</title>
|
|
4
|
+
<desc id="desc">A cyan-to-violet F with a continuous audio waveform, a bright icy-cyan crest and three tempo bars. Vector paths only.</desc>
|
|
5
|
+
<defs>
|
|
6
|
+
<linearGradient xmlns="http://www.w3.org/2000/svg" id="body" gradientUnits="userSpaceOnUse" x1="310" y1="1020" x2="780" y2="485">
|
|
7
|
+
<stop offset="0" stop-color="#00D7DD" />
|
|
8
|
+
<stop offset="0.36" stop-color="#047FBF" />
|
|
9
|
+
<stop offset="0.72" stop-color="#064397" />
|
|
10
|
+
<stop offset="1" stop-color="#022966" />
|
|
11
|
+
</linearGradient>
|
|
12
|
+
<linearGradient xmlns="http://www.w3.org/2000/svg" id="top" gradientUnits="userSpaceOnUse" x1="420" y1="360" x2="1130" y2="420">
|
|
13
|
+
<stop offset="0" stop-color="#00FFE0" />
|
|
14
|
+
<stop offset="0.48" stop-color="#009BFF" />
|
|
15
|
+
<stop offset="1" stop-color="#9426FF" />
|
|
16
|
+
</linearGradient>
|
|
17
|
+
<linearGradient xmlns="http://www.w3.org/2000/svg" id="lower" gradientUnits="userSpaceOnUse" x1="290" y1="1030" x2="830" y2="750">
|
|
18
|
+
<stop offset="0" stop-color="#00F2DC" />
|
|
19
|
+
<stop offset="0.35" stop-color="#00C8F2" />
|
|
20
|
+
<stop offset="0.64" stop-color="#146CFF" />
|
|
21
|
+
<stop offset="1" stop-color="#9527FF" />
|
|
22
|
+
</linearGradient>
|
|
23
|
+
<linearGradient xmlns="http://www.w3.org/2000/svg" id="wave" gradientUnits="userSpaceOnUse" x1="310" y1="650" x2="1000" y2="535">
|
|
24
|
+
<stop offset="0" stop-color="#00FFE1" />
|
|
25
|
+
<stop offset="0.3" stop-color="#40F9FF" />
|
|
26
|
+
<stop offset="0.36" stop-color="#ACFFFF" />
|
|
27
|
+
<stop offset="0.52" stop-color="#27DBFF" />
|
|
28
|
+
<stop offset="0.78" stop-color="#628EFF" />
|
|
29
|
+
<stop offset="1" stop-color="#BE31FF" />
|
|
30
|
+
</linearGradient>
|
|
31
|
+
<linearGradient xmlns="http://www.w3.org/2000/svg" id="tempo" gradientUnits="userSpaceOnUse" x1="770" y1="650" x2="990" y2="510">
|
|
32
|
+
<stop offset="0" stop-color="#28C8FF" />
|
|
33
|
+
<stop offset="0.5" stop-color="#8E8AFF" />
|
|
34
|
+
<stop offset="1" stop-color="#BF27FF" />
|
|
35
|
+
</linearGradient>
|
|
36
|
+
</defs>
|
|
37
|
+
<path fill="url(#body)" d="M260 1058 L446 545 C455 517 470 493 489 480 L466 459 L495 371 C514 319 552 294 608 294 H1118 C1139 294 1137 306 1123 330 L1097 374 C1079 407 1051 421 1012 421 H711 C676 421 661 432 649 465 L624 532 H903 C924 532 936 519 949 517 L983 516 C998 516 1002 525 994 540 L962 605 C947 637 922 652 885 655 L604 689 C576 695 556 711 540 738 H829 C847 738 850 744 841 760 L811 812 C798 835 776 844 745 844 H519 C489 844 475 861 462 890 L430 970 C408 1020 348 1052 260 1058 Z" />
|
|
38
|
+
<path fill="url(#top)" d="M371 579 L446 379 C466 326 514 294 575 294 H1123 C1144 294 1136 310 1123 330 L1097 374 C1079 407 1051 421 1012 421 H701 C639 421 602 421 557 444 C474 484 423 543 371 579 Z" />
|
|
39
|
+
<path fill="url(#lower)" d="M260 1058 C294 980 342 849 398 778 C437 725 481 693 532 692 C557 695 574 711 588 738 H829 C847 738 850 744 841 760 L811 812 C798 835 776 844 745 844 H519 C489 844 475 861 462 890 L430 970 C408 1020 348 1052 260 1058 Z" />
|
|
40
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="url(#wave)" d="M118 651 L318 642 C331 641 342 644 352 649 L352 659 C338 664 330 666 316 665 L118 660 C110 660 108 655 118 651 Z" />
|
|
41
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="none" stroke="url(#wave)" stroke-width="66" stroke-linecap="round" stroke-linejoin="round" opacity="0.06" d="M319 654 C351 654 363 628 385 599 C397 583 405 577 413 583 C427 596 440 641 456 669 C466 688 481 688 490 665 L533 514 C540 491 550 487 558 505 C571 535 573 604 584 635 C589 651 600 656 612 643 L646 610 C655 601 664 605 674 617 C689 635 708 644 725 637 C735 633 742 620 746 606 L760 568" />
|
|
42
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="none" stroke="url(#wave)" stroke-width="48" stroke-linecap="round" stroke-linejoin="round" opacity="0.14" d="M319 654 C351 654 363 628 385 599 C397 583 405 577 413 583 C427 596 440 641 456 669 C466 688 481 688 490 665 L533 514 C540 491 550 487 558 505 C571 535 573 604 584 635 C589 651 600 656 612 643 L646 610 C655 601 664 605 674 617 C689 635 708 644 725 637 C735 633 742 620 746 606 L760 568" />
|
|
43
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#052C69" stroke-width="36" stroke-linecap="round" stroke-linejoin="round" opacity="0.92" d="M319 654 C351 654 363 628 385 599 C397 583 405 577 413 583 C427 596 440 641 456 669 C466 688 481 688 490 665 L533 514 C540 491 550 487 558 505 C571 535 573 604 584 635 C589 651 600 656 612 643 L646 610 C655 601 664 605 674 617 C689 635 708 644 725 637 C735 633 742 620 746 606 L760 568" />
|
|
44
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="none" stroke="url(#wave)" stroke-width="23" stroke-linecap="round" stroke-linejoin="round" opacity="1" d="M319 654 C351 654 363 628 385 599 C397 583 405 577 413 583 C427 596 440 641 456 669 C466 688 481 688 490 665 L533 514 C540 491 550 487 558 505 C571 535 573 604 584 635 C589 651 600 656 612 643 L646 610 C655 601 664 605 674 617 C689 635 708 644 725 637 C735 633 742 620 746 606 L760 568" />
|
|
45
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#D5FFFF" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.66" d="M509 603 L534 514 C541 492 550 490 558 508 C567 532 571 579 575 605" />
|
|
46
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="url(#tempo)" d="M777 639 C770 639 768 634 771 627 L805 549 C808 542 812 540 820 540 H839 C847 540 850 546 847 553 L815 631 C812 638 808 641 801 641 Z" />
|
|
47
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="url(#tempo)" d="M840 639 C833 639 831 634 834 627 L875 531 C878 524 883 522 891 522 H916 C924 522 926 528 923 535 L882 631 C879 638 875 641 867 641 Z" />
|
|
48
|
+
<path xmlns="http://www.w3.org/2000/svg" fill="url(#tempo)" d="M904 639 C897 639 895 634 898 627 L941 526 C944 519 950 516 958 516 H984 C993 516 996 523 992 531 L952 624 C947 635 938 641 927 641 Z" />
|
|
49
|
+
</svg>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Collections.Generic;
|
|
4
|
+
using System.Windows;
|
|
5
|
+
using System.Windows.Controls;
|
|
6
|
+
using System.Windows.Media.Imaging;
|
|
7
|
+
|
|
8
|
+
namespace DrumBuddy
|
|
9
|
+
{
|
|
10
|
+
public sealed class HomeView : ScrollViewer
|
|
11
|
+
{
|
|
12
|
+
private readonly List<Button> tiles = new List<Button>();
|
|
13
|
+
public HomeView(Action<string> open)
|
|
14
|
+
{
|
|
15
|
+
VerticalScrollBarVisibility = ScrollBarVisibility.Auto; HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
|
|
16
|
+
var page = new StackPanel { Margin = new Thickness(12, 14, 12, 16) }; Content = page;
|
|
17
|
+
var hero = new DockPanel { Margin = new Thickness(0, 0, 0, 28) };
|
|
18
|
+
var mark = new Image { Width = 88, Height = 88, Margin = new Thickness(0, 0, 20, 0), Source = BrandArtwork.Raster() }; DockPanel.SetDock(mark, Dock.Left); hero.Children.Add(mark);
|
|
19
|
+
var heading = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; heading.Children.Add(WorkspaceUi.Text("Your workspace", 30)); heading.Children.Add(WorkspaceUi.Text("Choose a tool, or return to your timeline.", 15)); hero.Children.Add(heading); page.Children.Add(hero);
|
|
20
|
+
var apps = new WrapPanel(); page.Children.Add(apps);
|
|
21
|
+
string[,] entries = { { "editor", "Editor + Record", "Record, arrange and edit audio across 16 tracks." }, { "converter", "Converter", "Change audio format and sample rate for other devices." }, { "player", "Media Player", "Listen to PCM, float WAV and other audio formats." }, { "control", "R16 Control", "Open the hardware panel, mixer and MIDI controls." }, { "audio", "Audio devices", "Choose speakers, headphones and recording inputs." }, { "keybinds", "Keybinds", "Make the keyboard shortcuts work your way." } };
|
|
22
|
+
for (int i = 0; i < entries.GetLength(0); i++) {
|
|
23
|
+
string action = entries[i, 0]; var body = new StackPanel { Margin = new Thickness(18, 16, 18, 16) };
|
|
24
|
+
var name = WorkspaceUi.Text(entries[i, 1], 20); name.FontWeight = FontWeights.SemiBold; name.Margin = new Thickness(0, 0, 0, 12); body.Children.Add(name);
|
|
25
|
+
var detail = WorkspaceUi.Text(entries[i, 2], 14); detail.Foreground = WorkspaceUi.Color("#526771"); body.Children.Add(detail);
|
|
26
|
+
var button = new Button { Content = body, Height = 134, Margin = new Thickness(0, 0, 12, 12), Background = WorkspaceUi.Color("#FFFFFF"), Foreground = WorkspaceUi.Color("#182C37"), BorderBrush = WorkspaceUi.Color("#C3CED0"), HorizontalContentAlignment = HorizontalAlignment.Stretch, Padding = new Thickness(0) };
|
|
27
|
+
button.Click += delegate { open(action); }; tiles.Add(button); apps.Children.Add(button);
|
|
28
|
+
}
|
|
29
|
+
SizeChanged += delegate { double available = Math.Max(280, ActualWidth - 40); int columns = available >= 1000 ? 3 : available >= 620 ? 2 : 1; foreach (var tile in tiles) tile.Width = Math.Max(240, available / columns - 12); };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|