@pro-gram/flux 0.0.0-stage → 1.5.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 +32 -0
- package/README.md +102 -2
- package/bin/flux.js +29 -0
- package/dependencies.json +14 -0
- package/package.json +60 -6
- package/payload/DrumBuddy-Setup.exe +0 -0
- package/scripts/install.js +58 -0
- package/scripts/version.js +12 -0
- package/source/AssetBootstrap.cs +49 -0
- package/source/Audio.cs +453 -0
- package/source/AudioDeviceSettings.cs +126 -0
- package/source/AudioInputs.cs +50 -0
- package/source/AudioOutputs.cs +327 -0
- package/source/AudioWorkspace.cs +71 -0
- package/source/BrandArtwork.cs +71 -0
- package/source/Build Instructions.txt +59 -0
- package/source/Build.ps1 +123 -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/EditorView.cs +626 -0
- package/source/Fetch-Dependencies.ps1 +15 -0
- package/source/FloatRecording.cs +99 -0
- package/source/FloatRecordingTests.cs +170 -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 +219 -0
- package/source/InterfaceTests.cs +166 -0
- package/source/LICENSE.txt +23 -0
- package/source/Logo Prompt.txt +16 -0
- package/source/Main.cs +781 -0
- package/source/Main.xaml +292 -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 +134 -0
- package/source/Quick Start.txt +77 -0
- package/source/SelfTest.cs +78 -0
- package/source/SettingsWindow.cs +208 -0
- package/source/Setup.cs +192 -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 +32 -0
- package/source/Timeline.cs +261 -0
- package/source/TimelineGestures.cs +57 -0
- package/source/Uninstall.cs +69 -0
- package/source/WorkspaceTests.cs +198 -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,327 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Threading;
|
|
5
|
+
using System.Collections.Generic;
|
|
6
|
+
using System.Runtime.InteropServices;
|
|
7
|
+
using System.Windows;
|
|
8
|
+
using System.Windows.Controls;
|
|
9
|
+
using NAudio.CoreAudioApi;
|
|
10
|
+
using NAudio;
|
|
11
|
+
using NAudio.Wave;
|
|
12
|
+
using NAudio.Wave.SampleProviders;
|
|
13
|
+
|
|
14
|
+
namespace DrumBuddy
|
|
15
|
+
{
|
|
16
|
+
// Channel conversion and resampling happen in managed code before shared playback.
|
|
17
|
+
internal sealed class EndpointSamples : ISampleProvider
|
|
18
|
+
{
|
|
19
|
+
private readonly ISampleProvider source;
|
|
20
|
+
private readonly WaveFormat format;
|
|
21
|
+
private float[] input = new float[0];
|
|
22
|
+
public EndpointSamples(ISampleProvider source, int channels)
|
|
23
|
+
{
|
|
24
|
+
if (source.WaveFormat.Channels < 1 || source.WaveFormat.Channels > 2 || channels < 1 || channels > 32)
|
|
25
|
+
throw new ArgumentException("Unsupported playback channel layout.");
|
|
26
|
+
this.source = source; format = WaveFormat.CreateIeeeFloatWaveFormat(source.WaveFormat.SampleRate, channels);
|
|
27
|
+
}
|
|
28
|
+
public WaveFormat WaveFormat { get { return format; } }
|
|
29
|
+
public int Read(float[] buffer, int offset, int count)
|
|
30
|
+
{
|
|
31
|
+
int sourceChannels = source.WaveFormat.Channels, frames = count / format.Channels;
|
|
32
|
+
int needed = frames * sourceChannels;
|
|
33
|
+
if (input.Length < needed) input = new float[needed];
|
|
34
|
+
int read = source.Read(input, 0, needed), got = read / sourceChannels;
|
|
35
|
+
Array.Clear(buffer, offset, got * format.Channels);
|
|
36
|
+
for (int frame = 0; frame < got; frame++) {
|
|
37
|
+
float left = input[frame * sourceChannels], right = input[frame * sourceChannels + (sourceChannels == 1 ? 0 : 1)];
|
|
38
|
+
if (float.IsNaN(left) || float.IsInfinity(left)) left = 0;
|
|
39
|
+
if (float.IsNaN(right) || float.IsInfinity(right)) right = 0;
|
|
40
|
+
if (format.Channels == 1) buffer[offset + frame] = Math.Max(-1, Math.Min(1, left * .5f + right * .5f));
|
|
41
|
+
else {
|
|
42
|
+
buffer[offset + frame * format.Channels] = Math.Max(-1, Math.Min(1, left));
|
|
43
|
+
buffer[offset + frame * format.Channels + 1] = Math.Max(-1, Math.Min(1, right));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return got * format.Channels;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
internal sealed class CountedPlayback : ISampleProvider
|
|
51
|
+
{
|
|
52
|
+
private readonly ISampleProvider source;
|
|
53
|
+
public long Frames;
|
|
54
|
+
public CountedPlayback(ISampleProvider source) { this.source = source; }
|
|
55
|
+
public WaveFormat WaveFormat { get { return source.WaveFormat; } }
|
|
56
|
+
public int Read(float[] buffer, int offset, int count)
|
|
57
|
+
{ int read = source.Read(buffer, offset, count); Interlocked.Add(ref Frames, read / WaveFormat.Channels); return read; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
internal sealed class EndpointWaveProvider : IWaveProvider
|
|
61
|
+
{
|
|
62
|
+
private readonly ISampleProvider source;
|
|
63
|
+
private readonly bool floating;
|
|
64
|
+
private float[] bufferSamples = new float[0];
|
|
65
|
+
public WaveFormat WaveFormat { get; private set; }
|
|
66
|
+
public EndpointWaveProvider(ISampleProvider samples) : this(samples, null) { }
|
|
67
|
+
public EndpointWaveProvider(ISampleProvider samples, WaveFormat endpointFormat)
|
|
68
|
+
{
|
|
69
|
+
source = samples;
|
|
70
|
+
WaveFormat = endpointFormat ?? (samples.WaveFormat.Channels > 2
|
|
71
|
+
? new WaveFormatExtensible(samples.WaveFormat.SampleRate, 32, samples.WaveFormat.Channels)
|
|
72
|
+
: samples.WaveFormat);
|
|
73
|
+
var extensible = WaveFormat as WaveFormatExtensible;
|
|
74
|
+
var standard = extensible == null ? WaveFormat : extensible.ToStandardWaveFormat();
|
|
75
|
+
floating = standard.Encoding == WaveFormatEncoding.IeeeFloat && standard.BitsPerSample == 32;
|
|
76
|
+
bool pcm = standard.Encoding == WaveFormatEncoding.Pcm && new[] { 16, 24, 32 }.Contains(standard.BitsPerSample);
|
|
77
|
+
if ((!floating && !pcm) || WaveFormat.Channels != samples.WaveFormat.Channels || WaveFormat.SampleRate != samples.WaveFormat.SampleRate)
|
|
78
|
+
throw new ArgumentException("The endpoint format does not match the adapted playback audio.");
|
|
79
|
+
}
|
|
80
|
+
public int Read(byte[] buffer, int offset, int count)
|
|
81
|
+
{
|
|
82
|
+
int needed = count / WaveFormat.BlockAlign * WaveFormat.Channels;
|
|
83
|
+
if (bufferSamples.Length < needed) bufferSamples = new float[needed];
|
|
84
|
+
int read = source.Read(bufferSamples, 0, needed);
|
|
85
|
+
if (floating) { Buffer.BlockCopy(bufferSamples, 0, buffer, offset, read * 4); return read * 4; }
|
|
86
|
+
int bytes = WaveFormat.BitsPerSample / 8;
|
|
87
|
+
double scale = Math.Pow(2, WaveFormat.BitsPerSample - 1);
|
|
88
|
+
for (int i = 0; i < read; i++) {
|
|
89
|
+
double value = float.IsNaN(bufferSamples[i]) || float.IsInfinity(bufferSamples[i]) ? 0 : bufferSamples[i];
|
|
90
|
+
long pcm = (long)Math.Max(-scale, Math.Min(scale - 1, Math.Round(value * scale, MidpointRounding.AwayFromZero)));
|
|
91
|
+
for (int b = 0; b < bytes; b++) buffer[offset + i * bytes + b] = (byte)(pcm >> (8 * b));
|
|
92
|
+
}
|
|
93
|
+
return read * bytes;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
internal static class WindowsWaveDevices
|
|
98
|
+
{
|
|
99
|
+
[DllImport("winmm.dll")] private static extern uint waveOutMessage(IntPtr device, uint message, IntPtr value, IntPtr size);
|
|
100
|
+
[DllImport("winmm.dll")] private static extern uint waveInMessage(IntPtr device, uint message, IntPtr value, IntPtr size);
|
|
101
|
+
[DllImport("winmm.dll")] internal static extern int waveInGetNumDevs();
|
|
102
|
+
[DllImport("winmm.dll", CharSet = CharSet.Auto)] private static extern int waveInGetDevCaps(IntPtr device, out WaveInCapabilities caps, int size);
|
|
103
|
+
[DllImport("winmm.dll")] private static extern int waveInOpen(IntPtr handle, IntPtr device, [In, MarshalAs(UnmanagedType.LPStruct)] WaveFormat format, IntPtr callback, IntPtr instance, uint flags);
|
|
104
|
+
internal static bool SupportsInput(int device, int rate, int channels)
|
|
105
|
+
{ return waveInOpen(IntPtr.Zero, (IntPtr)device, new WaveFormat(rate, 16, channels), IntPtr.Zero, IntPtr.Zero, 1) == 0; }
|
|
106
|
+
internal static WaveInCapabilities InputCaps(int device)
|
|
107
|
+
{
|
|
108
|
+
WaveInCapabilities caps;
|
|
109
|
+
MmException.Try((MmResult)waveInGetDevCaps((IntPtr)device, out caps, Marshal.SizeOf(typeof(WaveInCapabilities))), "waveInGetDevCaps"); return caps;
|
|
110
|
+
}
|
|
111
|
+
private static uint Message(int device, bool capture, uint message, IntPtr value, IntPtr size)
|
|
112
|
+
{ return capture ? waveInMessage((IntPtr)device, message, value, size) : waveOutMessage((IntPtr)device, message, value, size); }
|
|
113
|
+
internal static string EndpointId(int device, bool capture = false)
|
|
114
|
+
{
|
|
115
|
+
IntPtr size = Marshal.AllocHGlobal(IntPtr.Size), text = IntPtr.Zero;
|
|
116
|
+
try {
|
|
117
|
+
Marshal.WriteIntPtr(size, IntPtr.Zero);
|
|
118
|
+
// mmddk.h: DRV_RESERVED + 18 (size) and + 17 (endpoint ID).
|
|
119
|
+
if (Message(device, capture, 0x812, size, IntPtr.Zero) != 0) return null;
|
|
120
|
+
int bytes = Marshal.ReadInt32(size);
|
|
121
|
+
if (bytes < 2 || bytes > 65536 || bytes % 2 != 0) return null;
|
|
122
|
+
text = Marshal.AllocHGlobal(bytes);
|
|
123
|
+
if (Message(device, capture, 0x811, text, (IntPtr)bytes) != 0) return null;
|
|
124
|
+
return Marshal.PtrToStringUni(text);
|
|
125
|
+
}
|
|
126
|
+
finally { if (text != IntPtr.Zero) Marshal.FreeHGlobal(text); Marshal.FreeHGlobal(size); }
|
|
127
|
+
}
|
|
128
|
+
internal static int Find(string endpoint, bool capture = false)
|
|
129
|
+
{
|
|
130
|
+
for (int i = 0; i < (capture ? waveInGetNumDevs() : OutputDevices.waveOutGetNumDevs()); i++)
|
|
131
|
+
if (string.Equals(endpoint, EndpointId(i, capture), StringComparison.OrdinalIgnoreCase)) return i;
|
|
132
|
+
return -2;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
internal sealed class PlaybackRoute : IDisposable
|
|
137
|
+
{
|
|
138
|
+
public IWavePlayer Player;
|
|
139
|
+
public MMDevice Endpoint;
|
|
140
|
+
public CountedPlayback Counter;
|
|
141
|
+
public WaveFormat PlaybackFormat;
|
|
142
|
+
public string Description;
|
|
143
|
+
public double Seconds
|
|
144
|
+
{
|
|
145
|
+
get {
|
|
146
|
+
var position = Player as IWavePosition;
|
|
147
|
+
if (position != null && Player.PlaybackState != PlaybackState.Stopped)
|
|
148
|
+
try { return position.GetPosition() / (double)(PlaybackFormat ?? Counter.WaveFormat).AverageBytesPerSecond; } catch { }
|
|
149
|
+
return Interlocked.Read(ref Counter.Frames) / (double)Counter.WaveFormat.SampleRate;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
public void Dispose()
|
|
153
|
+
{
|
|
154
|
+
var old = Player; Player = null;
|
|
155
|
+
if (old != null) { try { old.Stop(); } catch { } try { old.Dispose(); } catch { } }
|
|
156
|
+
if (Endpoint != null) { try { Endpoint.Dispose(); } catch { } Endpoint = null; }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
internal static class AudioOutputs
|
|
161
|
+
{
|
|
162
|
+
private static string preferredKey = "default";
|
|
163
|
+
public static int LatencyMilliseconds = 80;
|
|
164
|
+
public static event Action OutputChanged;
|
|
165
|
+
public static string PreferredKey {
|
|
166
|
+
get { return preferredKey; }
|
|
167
|
+
set { value = value ?? "default"; if (value == preferredKey) return; preferredKey = value; var changed = OutputChanged; if (changed != null) changed(); }
|
|
168
|
+
}
|
|
169
|
+
public static readonly string ErrorLog = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "DrumBuddy", "audio-output-errors.txt");
|
|
170
|
+
public static List<OutputChoice> Choices()
|
|
171
|
+
{
|
|
172
|
+
var choices = new List<OutputChoice> { new OutputChoice { Name = "Windows default speakers / headphones" } };
|
|
173
|
+
try {
|
|
174
|
+
using (var enumerator = new MMDeviceEnumerator()) {
|
|
175
|
+
try { using (var defaultDevice = enumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia)) choices[0].Name = "Windows default — " + defaultDevice.FriendlyName; } catch { }
|
|
176
|
+
foreach (var endpoint in enumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active)) {
|
|
177
|
+
using (endpoint) choices.Add(new OutputChoice { Name = endpoint.FriendlyName + " (Windows)", EndpointId = endpoint.ID });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch { /* The default entry keeps the picker usable if Windows Audio is stopped. */ }
|
|
182
|
+
choices.Add(new OutputChoice { Name = "Windows default - Compatibility (16-bit PCM)", Compatibility = true });
|
|
183
|
+
try {
|
|
184
|
+
for (int i = 0; i < OutputDevices.waveOutGetNumDevs(); i++) {
|
|
185
|
+
string endpoint = WindowsWaveDevices.EndpointId(i);
|
|
186
|
+
string name = choices.Where(c => !c.Compatibility && c.EndpointId == endpoint).Select(c => c.Name.Replace(" (Windows)", "")).FirstOrDefault() ?? OutputDevices.Name(i);
|
|
187
|
+
if (!string.IsNullOrEmpty(endpoint)) choices.Add(new OutputChoice { Name = name + " - Compatibility (16-bit PCM)", Compatibility = true, EndpointId = endpoint, Device = i });
|
|
188
|
+
}
|
|
189
|
+
} catch { }
|
|
190
|
+
try { foreach (string driver in AsioOut.GetDriverNames()) choices.Add(new OutputChoice { Name = driver + " (ASIO)", Driver = driver }); } catch { }
|
|
191
|
+
return choices;
|
|
192
|
+
}
|
|
193
|
+
public static ISampleProvider Adapt(ISampleProvider source, int rate, int channels)
|
|
194
|
+
{
|
|
195
|
+
if (rate < 8000 || rate > 384000) throw new ArgumentException("Unsupported playback sample rate.");
|
|
196
|
+
if (source.WaveFormat.SampleRate != rate) source = new WdlResamplingSampleProvider(source, rate);
|
|
197
|
+
return new EndpointSamples(source, channels);
|
|
198
|
+
}
|
|
199
|
+
public static PlaybackRoute Open(ISampleProvider source, OutputChoice choice)
|
|
200
|
+
{
|
|
201
|
+
choice = choice ?? new OutputChoice { Name = "Windows default speakers / headphones" };
|
|
202
|
+
if (choice.Driver != null) {
|
|
203
|
+
var route = new PlaybackRoute();
|
|
204
|
+
try {
|
|
205
|
+
var asio = new AsioOut(choice.Driver); route.Player = asio;
|
|
206
|
+
if (asio.DriverOutputChannelCount < 1) throw new IOException("This ASIO driver has no playback outputs.");
|
|
207
|
+
int rate = source.WaveFormat.SampleRate;
|
|
208
|
+
if (!asio.IsSampleRateSupported(rate)) rate = new[] { 48000, 44100, 96000 }.FirstOrDefault(asio.IsSampleRateSupported);
|
|
209
|
+
if (rate == 0) throw new IOException("The ASIO device is not offering a supported playback rate.");
|
|
210
|
+
route.Counter = new CountedPlayback(Adapt(source, rate, Math.Min(2, asio.DriverOutputChannelCount)));
|
|
211
|
+
var wire = new SampleToWaveProvider(route.Counter); route.PlaybackFormat = wire.WaveFormat;
|
|
212
|
+
asio.Init(wire); route.Description = choice.Driver + " - " + rate + " Hz"; return route;
|
|
213
|
+
}
|
|
214
|
+
catch (Exception ex) { route.Dispose(); throw DeviceError(choice.Name, ex); }
|
|
215
|
+
}
|
|
216
|
+
if (choice.Compatibility) return OpenCompatibility(source, choice);
|
|
217
|
+
Exception lastError = null; string endpointId = choice.EndpointId; var failures = new List<string>();
|
|
218
|
+
// Retry polling on the same selected endpoint if its driver rejects event sync.
|
|
219
|
+
foreach (bool events in new[] { true, false }) {
|
|
220
|
+
var route = new PlaybackRoute();
|
|
221
|
+
string stage = "find endpoint";
|
|
222
|
+
try {
|
|
223
|
+
using (var enumerator = new MMDeviceEnumerator())
|
|
224
|
+
route.Endpoint = string.IsNullOrEmpty(choice.EndpointId)
|
|
225
|
+
? enumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia)
|
|
226
|
+
: enumerator.GetDevice(choice.EndpointId);
|
|
227
|
+
endpointId = route.Endpoint.ID; stage = "create shared audio client";
|
|
228
|
+
var wasapi = new WasapiOut(route.Endpoint, AudioClientShareMode.Shared, events, Math.Max(40, Math.Min(250, LatencyMilliseconds))); route.Player = wasapi;
|
|
229
|
+
WaveFormat mix = wasapi.OutputWaveFormat;
|
|
230
|
+
route.Counter = new CountedPlayback(Adapt(source, mix.SampleRate, mix.Channels));
|
|
231
|
+
var wire = new EndpointWaveProvider(route.Counter, mix); route.PlaybackFormat = wire.WaveFormat;
|
|
232
|
+
stage = "initialize " + mix;
|
|
233
|
+
wasapi.Init(wire);
|
|
234
|
+
route.Description = route.Endpoint.FriendlyName + " · Windows shared audio · " + mix.SampleRate + " Hz";
|
|
235
|
+
return route;
|
|
236
|
+
}
|
|
237
|
+
catch (Exception ex) { lastError = ex; failures.Add("WASAPI " + (events ? "event" : "polling") + " / " + stage + ": " + ex); route.Dispose(); }
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
var result = OpenCompatibility(source, new OutputChoice { Name = choice.Name, Compatibility = true, EndpointId = endpointId });
|
|
241
|
+
WriteFailures(choice.Name, failures, "Recovered using " + result.Description); return result;
|
|
242
|
+
}
|
|
243
|
+
catch (Exception ex) { lastError = ex; failures.Add("Compatibility: " + ex); }
|
|
244
|
+
WriteFailures(choice.Name, failures, "Playback could not start");
|
|
245
|
+
throw DeviceError(choice.Name, lastError);
|
|
246
|
+
}
|
|
247
|
+
internal static PlaybackRoute OpenCompatibility(ISampleProvider source, OutputChoice choice)
|
|
248
|
+
{
|
|
249
|
+
int device = string.IsNullOrEmpty(choice.EndpointId) ? -1 : WindowsWaveDevices.Find(choice.EndpointId);
|
|
250
|
+
if (device == -2) throw new IOException("The selected output is no longer available. Click Refresh in Audio devices and select it again.");
|
|
251
|
+
Exception last = null;
|
|
252
|
+
foreach (int rate in new[] { 48000, 44100 }) {
|
|
253
|
+
var route = new PlaybackRoute();
|
|
254
|
+
try {
|
|
255
|
+
var player = new WaveOutEvent { DeviceNumber = device, DesiredLatency = Math.Max(100, Math.Min(250, LatencyMilliseconds)), NumberOfBuffers = 3 }; route.Player = player;
|
|
256
|
+
route.Counter = new CountedPlayback(Adapt(source, rate, 2));
|
|
257
|
+
var wire = new EndpointWaveProvider(route.Counter, new WaveFormat(rate, 16, 2)); route.PlaybackFormat = wire.WaveFormat;
|
|
258
|
+
player.Init(wire);
|
|
259
|
+
route.Description = (device == -1 ? "Windows default" : OutputDevices.Name(device)) + " - Compatibility / 16-bit PCM / " + rate + " Hz";
|
|
260
|
+
return route;
|
|
261
|
+
}
|
|
262
|
+
catch (Exception ex) { last = ex; route.Dispose(); }
|
|
263
|
+
}
|
|
264
|
+
throw new IOException("Windows PCM compatibility playback failed: " + last.Message + " (0x" + last.HResult.ToString("X8") + ")", last);
|
|
265
|
+
}
|
|
266
|
+
private static void WriteFailures(string name, IEnumerable<string> failures, string result)
|
|
267
|
+
{
|
|
268
|
+
try {
|
|
269
|
+
Directory.CreateDirectory(Path.GetDirectoryName(ErrorLog));
|
|
270
|
+
File.WriteAllText(ErrorLog, DateTimeOffset.Now.ToString("o") + " | " + name + "\r\n" + string.Join("\r\n\r\n", failures) + "\r\n\r\n" + result);
|
|
271
|
+
} catch { }
|
|
272
|
+
}
|
|
273
|
+
private static IOException DeviceError(string name, Exception error)
|
|
274
|
+
{
|
|
275
|
+
string detail = error == null ? "No playback device is available." : error.Message + " (0x" + error.HResult.ToString("X8") + ")";
|
|
276
|
+
return new IOException("Could not open " + name + ". " + detail + " Open Audio devices at the top of the app, click Refresh, and select your speakers/headphones. Try the Compatibility (16-bit PCM) entry. Save diagnostics there if it still fails.", error);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
internal sealed class AudioOutputPicker : Grid
|
|
281
|
+
{
|
|
282
|
+
private readonly ComboBox devices;
|
|
283
|
+
private readonly Action changed;
|
|
284
|
+
private readonly Action<string> status;
|
|
285
|
+
private bool updating;
|
|
286
|
+
public OutputChoice Selected { get { return devices.SelectedItem as OutputChoice; } }
|
|
287
|
+
public AudioOutputPicker(Action changed, Action<string> status)
|
|
288
|
+
{
|
|
289
|
+
this.changed = changed; this.status = status; Margin = new Thickness(0, 0, 0, 12);
|
|
290
|
+
ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); ColumnDefinitions.Add(new ColumnDefinition()); ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
|
291
|
+
var label = WorkspaceUi.Text("Audio output ", 14); Children.Add(label);
|
|
292
|
+
devices = new ComboBox { FontSize = 14, MinHeight = 34, Padding = new Thickness(5), MaxWidth = 520, HorizontalAlignment = HorizontalAlignment.Stretch };
|
|
293
|
+
SetColumn(devices, 1); Children.Add(devices);
|
|
294
|
+
var refresh = WorkspaceUi.Button("Refresh", delegate { Refresh(); }); refresh.Margin = new Thickness(8, 0, 0, 0); SetColumn(refresh, 2); Children.Add(refresh);
|
|
295
|
+
devices.SelectionChanged += delegate {
|
|
296
|
+
if (updating || Selected == null) return;
|
|
297
|
+
AudioOutputs.PreferredKey = Selected.Key; if (changed != null) changed();
|
|
298
|
+
};
|
|
299
|
+
Loaded += delegate { AudioOutputs.OutputChanged += SyncSelection; SyncSelection(); };
|
|
300
|
+
Unloaded += delegate { AudioOutputs.OutputChanged -= SyncSelection; };
|
|
301
|
+
Refresh();
|
|
302
|
+
}
|
|
303
|
+
private void SyncSelection()
|
|
304
|
+
{
|
|
305
|
+
if (!Dispatcher.CheckAccess()) { Dispatcher.BeginInvoke((Action)SyncSelection); return; }
|
|
306
|
+
var choices = devices.ItemsSource as IEnumerable<OutputChoice>;
|
|
307
|
+
if (choices == null) return;
|
|
308
|
+
var selected = choices.FirstOrDefault(c => c.Key == AudioOutputs.PreferredKey);
|
|
309
|
+
if (selected == null) { Refresh(AudioOutputs.PreferredKey); return; }
|
|
310
|
+
if (Selected != null && Selected.Key == selected.Key) return;
|
|
311
|
+
updating = true; devices.SelectedItem = selected; updating = false;
|
|
312
|
+
if (changed != null) changed();
|
|
313
|
+
}
|
|
314
|
+
private void Refresh(string desiredKey = null)
|
|
315
|
+
{
|
|
316
|
+
string key = desiredKey ?? (Selected == null ? AudioOutputs.PreferredKey : Selected.Key);
|
|
317
|
+
if (changed != null && devices.Items.Count > 0) changed();
|
|
318
|
+
updating = true;
|
|
319
|
+
try {
|
|
320
|
+
var choices = AudioOutputs.Choices(); devices.ItemsSource = choices;
|
|
321
|
+
devices.SelectedItem = choices.FirstOrDefault(c => c.Key == key) ?? choices[0];
|
|
322
|
+
}
|
|
323
|
+
catch (Exception ex) { if (status != null) status("Could not list audio outputs: " + ex.Message); }
|
|
324
|
+
finally { updating = false; }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Windows;
|
|
3
|
+
using System.Windows.Controls;
|
|
4
|
+
using System.Windows.Media;
|
|
5
|
+
using System.Windows.Threading;
|
|
6
|
+
using NAudio.Wave;
|
|
7
|
+
using NAudio.Wave.SampleProviders;
|
|
8
|
+
|
|
9
|
+
namespace DrumBuddy
|
|
10
|
+
{
|
|
11
|
+
internal static class WorkspaceUi
|
|
12
|
+
{
|
|
13
|
+
public static Brush Color(string value) { return ThemePalette.Brush(value); }
|
|
14
|
+
public static Button Button(string text, Action action)
|
|
15
|
+
{
|
|
16
|
+
var button = new Button { Content = text, MinHeight = 30, Padding = new Thickness(9, 4, 9, 4), FontSize = 13, Margin = new Thickness(0, 0, 6, 6), Background = Color("#FFFFFF"), Foreground = Color("#182C37"), BorderBrush = Color("#C3CED0") };
|
|
17
|
+
button.Click += delegate { action(); }; return button;
|
|
18
|
+
}
|
|
19
|
+
public static TextBlock Text(string text, double size)
|
|
20
|
+
{ return new TextBlock { Text = text, FontSize = Math.Max(11, size * .94), Foreground = Color("#182C37"), TextWrapping = TextWrapping.Wrap, VerticalAlignment = VerticalAlignment.Center }; }
|
|
21
|
+
public static string Clock(double seconds)
|
|
22
|
+
{ return TimeSpan.FromSeconds(Math.Max(0, seconds)).ToString(seconds >= 3600 ? @"h\:mm\:ss\.ff" : @"mm\:ss\.ff"); }
|
|
23
|
+
public static string AudioFilter = "Audio and video|*.wav;*.wave;*.bwf;*.rf64;*.mp3;*.mp4;*.m4a;*.aac;*.flac;*.ogg;*.opus;*.wma;*.aif;*.aiff;*.mov;*.mkv;*.webm|All files|*.*";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// All file formats are decoded before this Windows PCM playback boundary.
|
|
27
|
+
internal sealed class WorkspaceOutput : IDisposable
|
|
28
|
+
{
|
|
29
|
+
private readonly Dispatcher dispatcher;
|
|
30
|
+
private IWavePlayer device;
|
|
31
|
+
private PlaybackRoute route;
|
|
32
|
+
private GainProvider gain;
|
|
33
|
+
private double start, held;
|
|
34
|
+
public string Description { get { return route == null ? "" : route.Description; } }
|
|
35
|
+
public WorkspaceOutput(Dispatcher dispatcher) { this.dispatcher = dispatcher; }
|
|
36
|
+
public bool Playing { get { return device != null && device.PlaybackState == PlaybackState.Playing; } }
|
|
37
|
+
public bool Paused { get { return device != null && device.PlaybackState == PlaybackState.Paused; } }
|
|
38
|
+
public double Position
|
|
39
|
+
{
|
|
40
|
+
get {
|
|
41
|
+
if (device == null) return held;
|
|
42
|
+
try { return start + route.Seconds; } catch { return held; }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
public void Open(ISampleProvider source, double at, float volume, Action<Exception> ended, OutputChoice choice = null)
|
|
46
|
+
{
|
|
47
|
+
Stop(); start = held = at;
|
|
48
|
+
gain = new GainProvider(source) { Gain = volume };
|
|
49
|
+
try {
|
|
50
|
+
route = AudioOutputs.Open(gain, choice); var current = route.Player; device = current;
|
|
51
|
+
current.PlaybackStopped += delegate(object sender, StoppedEventArgs e) {
|
|
52
|
+
dispatcher.BeginInvoke((Action)delegate {
|
|
53
|
+
if (device != current) return;
|
|
54
|
+
Stop(); if (ended != null) ended(e.Exception);
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
current.Play();
|
|
58
|
+
}
|
|
59
|
+
catch { Stop(); throw; }
|
|
60
|
+
}
|
|
61
|
+
public void Pause() { if (device != null) device.Pause(); }
|
|
62
|
+
public void Resume() { if (device != null) device.Play(); }
|
|
63
|
+
public void Volume(float level) { if (gain != null) gain.Gain = level; }
|
|
64
|
+
public void Stop()
|
|
65
|
+
{
|
|
66
|
+
held = Position; device = null; gain = null;
|
|
67
|
+
if (route != null) { route.Dispose(); route = null; }
|
|
68
|
+
}
|
|
69
|
+
public void Dispose() { Stop(); }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Globalization;
|
|
4
|
+
using System.IO;
|
|
5
|
+
using System.Reflection;
|
|
6
|
+
using System.Windows;
|
|
7
|
+
using System.Windows.Media;
|
|
8
|
+
using System.Windows.Media.Imaging;
|
|
9
|
+
using System.Xml;
|
|
10
|
+
|
|
11
|
+
namespace DrumBuddy
|
|
12
|
+
{
|
|
13
|
+
public static class BrandArtwork
|
|
14
|
+
{
|
|
15
|
+
private static DrawingImage cached;
|
|
16
|
+
private static BitmapSource raster;
|
|
17
|
+
public static ImageSource Raster()
|
|
18
|
+
{
|
|
19
|
+
if (raster != null) return raster;
|
|
20
|
+
var source = new BitmapImage(); var embedded = Assembly.GetExecutingAssembly().GetManifestResourceStream("logo.png");
|
|
21
|
+
try {
|
|
22
|
+
source.BeginInit(); source.CacheOption = BitmapCacheOption.OnLoad;
|
|
23
|
+
if (embedded != null) source.StreamSource = embedded; else source.UriSource = new Uri(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logo.png"));
|
|
24
|
+
source.EndInit(); source.Freeze();
|
|
25
|
+
}
|
|
26
|
+
finally { if (embedded != null) embedded.Dispose(); }
|
|
27
|
+
raster = new CroppedBitmap(source, new Int32Rect(100, 280, 1050, 800)); raster.Freeze(); return raster;
|
|
28
|
+
}
|
|
29
|
+
// The SVG alternative uses paths and user-space linear gradients only.
|
|
30
|
+
// Render those directly as WPF vector geometry, without a bitmap fallback.
|
|
31
|
+
public static ImageSource Load()
|
|
32
|
+
{
|
|
33
|
+
if (cached != null) return cached;
|
|
34
|
+
var svg = new XmlDocument { XmlResolver = null };
|
|
35
|
+
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Flux-Mark.svg")) {
|
|
36
|
+
if (stream == null) throw new IOException("The Flux vector artwork is missing.");
|
|
37
|
+
using (var reader = XmlReader.Create(stream, new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null })) svg.Load(reader);
|
|
38
|
+
}
|
|
39
|
+
var ns = new XmlNamespaceManager(svg.NameTable); ns.AddNamespace("s", "http://www.w3.org/2000/svg");
|
|
40
|
+
var brushes = new Dictionary<string, Brush>();
|
|
41
|
+
foreach (XmlElement gradient in svg.SelectNodes("/s:svg/s:defs/s:linearGradient", ns)) {
|
|
42
|
+
var brush = new LinearGradientBrush { MappingMode = BrushMappingMode.Absolute,
|
|
43
|
+
StartPoint = new Point(Number(gradient, "x1"), Number(gradient, "y1")), EndPoint = new Point(Number(gradient, "x2"), Number(gradient, "y2")) };
|
|
44
|
+
foreach (XmlElement stop in gradient.SelectNodes("s:stop", ns)) brush.GradientStops.Add(new GradientStop((Color)ColorConverter.ConvertFromString(stop.GetAttribute("stop-color")), Number(stop, "offset")));
|
|
45
|
+
brush.Freeze(); brushes.Add(gradient.GetAttribute("id"), brush);
|
|
46
|
+
}
|
|
47
|
+
var drawing = new DrawingGroup(); string[] box = svg.DocumentElement.GetAttribute("viewBox").Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
|
|
48
|
+
var bounds = new Rect(Parse(box[0]), Parse(box[1]), Parse(box[2]), Parse(box[3]));
|
|
49
|
+
drawing.Children.Add(new GeometryDrawing(Brushes.Transparent, null, new RectangleGeometry(bounds))); drawing.ClipGeometry = new RectangleGeometry(bounds);
|
|
50
|
+
foreach (XmlElement path in svg.SelectNodes("/s:svg/s:path", ns)) {
|
|
51
|
+
double opacity = path.HasAttribute("opacity") ? Number(path, "opacity") : 1;
|
|
52
|
+
Brush fill = Paint(path.GetAttribute("fill"), brushes, opacity); Pen pen = null;
|
|
53
|
+
if (path.HasAttribute("stroke")) {
|
|
54
|
+
pen = new Pen(Paint(path.GetAttribute("stroke"), brushes, opacity), Number(path, "stroke-width"));
|
|
55
|
+
if (path.GetAttribute("stroke-linecap") == "round") pen.StartLineCap = pen.EndLineCap = PenLineCap.Round;
|
|
56
|
+
if (path.GetAttribute("stroke-linejoin") == "round") pen.LineJoin = PenLineJoin.Round;
|
|
57
|
+
}
|
|
58
|
+
drawing.Children.Add(new GeometryDrawing(fill, pen, Geometry.Parse("F1 " + path.GetAttribute("d"))));
|
|
59
|
+
}
|
|
60
|
+
drawing.Freeze(); cached = new DrawingImage(drawing); cached.Freeze(); return cached;
|
|
61
|
+
}
|
|
62
|
+
private static double Parse(string value) { return double.Parse(value, CultureInfo.InvariantCulture); }
|
|
63
|
+
private static Brush Paint(string value, Dictionary<string, Brush> brushes, double opacity)
|
|
64
|
+
{
|
|
65
|
+
if (value == "none" || string.IsNullOrEmpty(value)) return null;
|
|
66
|
+
Brush brush = value.StartsWith("url(#", StringComparison.Ordinal) ? brushes[value.Substring(5, value.Length - 6)].Clone() : new SolidColorBrush((Color)ColorConverter.ConvertFromString(value));
|
|
67
|
+
brush.Opacity = opacity; return brush;
|
|
68
|
+
}
|
|
69
|
+
private static double Number(XmlElement element, string attribute) { return Parse(element.GetAttribute(attribute)); }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
FLUX SOURCE (DRUM BUDDY COMPATIBLE UPDATE)
|
|
2
|
+
|
|
3
|
+
Build on Windows 10/11 x64 with .NET Framework 4.8.
|
|
4
|
+
No Visual Studio or .NET SDK is required: Build.ps1 uses the C# compiler
|
|
5
|
+
included with .NET Framework.
|
|
6
|
+
|
|
7
|
+
1. Run Fetch-Dependencies.ps1 in PowerShell. All downloads are pinned by SHA-256.
|
|
8
|
+
2. Run Build.ps1. The output is ..\build\DrumBuddy-Setup.exe and the portable
|
|
9
|
+
app folder ..\build\DrumBuddy.
|
|
10
|
+
|
|
11
|
+
Dependencies are pinned to reviewed SHA-256 hashes. The FFmpeg release URL is version-specific. If an upstream asset changes, review the replacement before changing its lock; do not disable verification.
|
|
12
|
+
|
|
13
|
+
Verification:
|
|
14
|
+
DrumBuddy.exe --self-test "C:\path\to\new-audio-test-folder"
|
|
15
|
+
DrumBuddy.exe --grid-self-test "C:\path\to\new-grid-test-folder"
|
|
16
|
+
DrumBuddy.exe --workspace-self-test "C:\path\to\new-editor-test-folder"
|
|
17
|
+
DrumBuddy.exe --float-self-test "C:\path\to\new-float-test-folder"
|
|
18
|
+
DrumBuddy.exe --output-self-test "C:\path\to\new-output-test-folder"
|
|
19
|
+
DrumBuddy.exe --interface-self-test "C:\path\to\new-interface-test-folder"
|
|
20
|
+
DrumBuddy.exe --conversion-self-test "C:\path\to\new-conversion-test-folder"
|
|
21
|
+
DrumBuddy.exe --audio-diagnostics "C:\path\to\Audio-Diagnostics.txt"
|
|
22
|
+
DrumBuddy-Setup.exe --verify-payload "C:\path\to\new-payload-check-folder"
|
|
23
|
+
|
|
24
|
+
The self-test creates real MP3 and MP4 samples, then checks decoding,
|
|
25
|
+
16/24-bit output, positive/negative gain, peak protection, safe file names,
|
|
26
|
+
preservation of existing/source files, cancellation, and WAV headers.
|
|
27
|
+
Payload verification extracts and checks every bundled file against SHA-256.
|
|
28
|
+
It does not install the app, install a driver, or modify Windows settings.
|
|
29
|
+
The workspace suite covers the 16-track editor, range/playhead gestures,
|
|
30
|
+
endpoint resampling/channel adaptation, PCM/float imports and exports,
|
|
31
|
+
and offscreen player/editor renders. The output suite opens the actual
|
|
32
|
+
default Windows endpoint and plays only silence; it never changes volume.
|
|
33
|
+
Windows speakers/headphones use shared WASAPI with the exact endpoint format,
|
|
34
|
+
with an endpoint-matched 16-bit PCM compatibility fallback. The output suite
|
|
35
|
+
checks both paths and queries microphone formats without opening capture.
|
|
36
|
+
Windows microphone buffer tests cover routing, saved samples and queue draining.
|
|
37
|
+
The interface suite checks keybind capture, saved/unsaved settings, theme
|
|
38
|
+
switching, integrated recording insertion and dark/light/compact layouts.
|
|
39
|
+
The general-conversion suite checks original-rate float/PCM WAV, FLAC,
|
|
40
|
+
MP3 and AAC outputs, headroom, cancellation and source-file preservation.
|
|
41
|
+
Use a fresh output directory for every suite run.
|
|
42
|
+
Native ASIO remains available. Physical recording and Realtek playback on the
|
|
43
|
+
affected PC still need a connected-device acceptance test.
|
|
44
|
+
|
|
45
|
+
ASIO recording must be acceptance-tested on a physical Zoom R16.
|
|
46
|
+
Check all eight inputs, 1+2 stereo, individual mono inputs, 16/24-bit saved
|
|
47
|
+
files, stop/save, direct monitoring, and safe handling of a disconnected USB cable.
|
|
48
|
+
|
|
49
|
+
The app is WPF/.NET Framework with NAudio 2.2.1. FFmpeg decodes local audio
|
|
50
|
+
and video into cached 44.1-kHz floating-point WAV, preserving mono and
|
|
51
|
+
downmixing other channel layouts to stereo. The app scans every decoded
|
|
52
|
+
sample for the gain ceiling, then writes classic 16/24-bit PCM WAV files.
|
|
53
|
+
Capturing uses an ASIO callback, bounded audio queue, and background writer;
|
|
54
|
+
it writes eight separate mono files or one stereo/mono file as selected.
|
|
55
|
+
|
|
56
|
+
The installer is a per-user native Windows executable with an embedded ZIP
|
|
57
|
+
and per-file integrity manifest. With -BootstrapOnly, FFmpeg and the panel reference image are downloaded and verified at first setup, then cached. The Zoom driver is not bundled or downloaded; Install Drivers opens its official support page and a setup tutorial.
|
|
58
|
+
The uninstaller removes only files in the installation's saved file list;
|
|
59
|
+
it preserves user audio, settings, unknown files, and the Zoom driver.
|