@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,198 @@
|
|
|
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.Reflection;
|
|
8
|
+
using System.Windows;
|
|
9
|
+
using System.Windows.Markup;
|
|
10
|
+
using System.Windows.Media;
|
|
11
|
+
using System.Windows.Media.Imaging;
|
|
12
|
+
using NAudio.Wave;
|
|
13
|
+
using NAudio.Wave.SampleProviders;
|
|
14
|
+
|
|
15
|
+
namespace DrumBuddy
|
|
16
|
+
{
|
|
17
|
+
public static class WorkspaceTests
|
|
18
|
+
{
|
|
19
|
+
private static readonly List<string> log = new List<string>();
|
|
20
|
+
private static void Check(bool ok, string message) { if (!ok) throw new Exception(message); log.Add("PASS " + message); }
|
|
21
|
+
private static Clip Source(string root, string name, float[] values, int channels)
|
|
22
|
+
{
|
|
23
|
+
string path = Path.Combine(root, name + ".wav");
|
|
24
|
+
using (var writer = new FloatWaveWriter(path, channels, Audio.Rate)) writer.Write(values, values.Length, 1);
|
|
25
|
+
return Audio.Load(path, Path.Combine(root, "cache"), CancellationToken.None, null);
|
|
26
|
+
}
|
|
27
|
+
private static float[] Render(EditorSession session, IDictionary<string, Clip> decoded)
|
|
28
|
+
{
|
|
29
|
+
using (var mix = new TimelineMix(session, decoded)) {
|
|
30
|
+
var result = new List<float>(); var block = new float[126]; int read;
|
|
31
|
+
while ((read = mix.Read(block, 0, block.Length)) > 0) result.AddRange(block.Take(read));
|
|
32
|
+
return result.ToArray();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
private static void Picture(FrameworkElement view, string path, int width, int height)
|
|
36
|
+
{
|
|
37
|
+
view.Measure(new Size(width, height)); view.Arrange(new Rect(0, 0, width, height)); view.UpdateLayout();
|
|
38
|
+
var image = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
|
|
39
|
+
var background = new DrawingVisual(); using (var drawing = background.RenderOpen()) drawing.DrawRectangle(WorkspaceUi.Color("#F3F5F5"), null, new Rect(0, 0, width, height));
|
|
40
|
+
image.Render(background); image.Render(view);
|
|
41
|
+
var png = new PngBitmapEncoder(); png.Frames.Add(BitmapFrame.Create(image)); using (var file = File.Create(path)) png.Save(file);
|
|
42
|
+
}
|
|
43
|
+
public static int Run(string[] args)
|
|
44
|
+
{
|
|
45
|
+
string root = Path.GetFullPath(args.Length > 0 ? args[0] : Path.Combine(Path.GetTempPath(), "DrumBuddyWorkspace_" + Guid.NewGuid().ToString("N")));
|
|
46
|
+
Directory.CreateDirectory(root); log.Clear();
|
|
47
|
+
try {
|
|
48
|
+
PlaybackTests.Offline(Check);
|
|
49
|
+
var preference = new Preferences { AudioOutputKey = "compat:default", AudioInputKey = "input:microphone" };
|
|
50
|
+
var serializer = new System.Xml.Serialization.XmlSerializer(typeof(Preferences));
|
|
51
|
+
using (var xml = new StringWriter()) {
|
|
52
|
+
serializer.Serialize(xml, preference);
|
|
53
|
+
using (var read = new StringReader(xml.ToString())) {
|
|
54
|
+
var reopenedPreference = (Preferences)serializer.Deserialize(read);
|
|
55
|
+
Check(reopenedPreference.AudioInputKey == preference.AudioInputKey && reopenedPreference.AudioOutputKey == preference.AudioOutputKey,
|
|
56
|
+
"Save and reload both microphone and compatibility-output selections");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
var pickerA = new AudioOutputPicker(delegate { }, delegate(string text) { }); var pickerB = new AudioOutputPicker(delegate { }, delegate(string text) { });
|
|
60
|
+
string previousOutput = AudioOutputs.PreferredKey;
|
|
61
|
+
pickerA.RaiseEvent(new RoutedEventArgs(FrameworkElement.LoadedEvent)); pickerB.RaiseEvent(new RoutedEventArgs(FrameworkElement.LoadedEvent));
|
|
62
|
+
try {
|
|
63
|
+
AudioOutputs.PreferredKey = "compat:default";
|
|
64
|
+
Check(pickerA.Selected != null && pickerB.Selected != null && pickerA.Selected.Key == "compat:default" && pickerB.Selected.Key == "compat:default",
|
|
65
|
+
"Synchronize loaded playback selectors when the shared output changes");
|
|
66
|
+
}
|
|
67
|
+
finally { AudioOutputs.PreferredKey = previousOutput; pickerA.RaiseEvent(new RoutedEventArgs(FrameworkElement.UnloadedEvent)); pickerB.RaiseEvent(new RoutedEventArgs(FrameworkElement.UnloadedEvent)); }
|
|
68
|
+
float[] precise = Enumerable.Range(0, 500).Select(i => (float)(i + 1) / 100).ToArray();
|
|
69
|
+
Clip ramp = Source(root, "Precise float source", precise, 1);
|
|
70
|
+
byte[] original = File.ReadAllBytes(ramp.Source);
|
|
71
|
+
var decoded = new Dictionary<string, Clip>(StringComparer.OrdinalIgnoreCase) { { ramp.Source, ramp } };
|
|
72
|
+
var session = new EditorSession();
|
|
73
|
+
session.Regions.Add(new TimelineRegion { Source = ramp.Source, Name = "Ramp", Track = 0, Start = 100, SourceStart = 20, Length = 100 });
|
|
74
|
+
var baseline = Render(session, decoded);
|
|
75
|
+
Check(baseline.Length == 400 && baseline.Take(200).All(v => v == 0), "Render silence before a region and stop exactly at the timeline end");
|
|
76
|
+
Check(Math.Abs(baseline[200] - precise[20]) < .000001 && baseline[200] == baseline[201] && Math.Abs(baseline[398] - precise[119]) < .000001,
|
|
77
|
+
"Use exact source-frame offsets and duplicate mono correctly into stereo");
|
|
78
|
+
var sixteen = new EditorSession();
|
|
79
|
+
for (int i = 0; i < 16; i++) sixteen.Regions.Add(new TimelineRegion { Source = ramp.Source, Name = "Input " + (i + 1), Track = i, Start = 0, SourceStart = 0, Length = 1 });
|
|
80
|
+
Check(Math.Abs(Render(sixteen, decoded)[0] - precise[0] * 16) < .000001, "Mix audio from all 16 editor tracks simultaneously");
|
|
81
|
+
Clip stereo = Source(root, "Stereo separation", Enumerable.Range(0, 512).Select(i => i % 2 == 0 ? .5f : -.25f).ToArray(), 2);
|
|
82
|
+
var stereoState = new EditorSession(); stereoState.Regions.Add(new TimelineRegion { Source = stereo.Source, Name = "Stereo", Track = 0, Start = 0, SourceStart = 10, Length = 100 });
|
|
83
|
+
var stereoAudio = Render(stereoState, new Dictionary<string, Clip> { { stereo.Source, stereo } });
|
|
84
|
+
Check(stereoAudio.Where((v, i) => i % 2 == 0).All(v => v == .5f) && stereoAudio.Where((v, i) => i % 2 == 1).All(v => v == -.25f), "Preserve stereo channel separation when trimming and mixing");
|
|
85
|
+
using (var mix = new TimelineMix(session, decoded)) {
|
|
86
|
+
mix.Seek(150); var block = new float[4]; mix.Read(block, 0, 4);
|
|
87
|
+
Check(Math.Abs(block[0] - precise[70]) < .000001 && mix.Position == 152, "Seek into a region without losing source position");
|
|
88
|
+
}
|
|
89
|
+
var split = session.Copy(); TimelineEdits.Split(split, 0, false, 150);
|
|
90
|
+
Check(split.Regions.Count == 2 && split.Regions[1].Start == 150 && split.Regions[1].SourceStart == 70 && Render(split, decoded).SequenceEqual(baseline),
|
|
91
|
+
"Split audio without changing any rendered sample or timing");
|
|
92
|
+
var cut = session.Copy(); TimelineEdits.Delete(cut, 0, false, 130, 170); var cutAudio = Render(cut, decoded);
|
|
93
|
+
Check(cut.Regions.Count == 2 && cut.Regions[0].Length == 30 && cut.Regions[1].Start == 170 && cut.Regions[1].SourceStart == 90,
|
|
94
|
+
"Cut the middle of a clip into correctly trimmed left and right regions");
|
|
95
|
+
Check(cutAudio.Skip(260).Take(80).All(v => v == 0) && cutAudio.Take(260).SequenceEqual(baseline.Take(260)) && cutAudio.Skip(340).SequenceEqual(baseline.Skip(340)),
|
|
96
|
+
"Leave the cut gap while preserving all unselected samples and alignment");
|
|
97
|
+
var copied = TimelineEdits.Copy(session, 0, false, 120, 180); var pasted = session.Copy();
|
|
98
|
+
TimelineEdits.Paste(pasted, copied, 15, 300); var lastTrack = pasted.Regions.Last();
|
|
99
|
+
Check(lastTrack.Track == 15 && lastTrack.Start == 300 && lastTrack.SourceStart == 40 && lastTrack.Length == 60,
|
|
100
|
+
"Copy a partial section and paste it into track 16 at the playhead");
|
|
101
|
+
Check(Math.Abs(Render(pasted, decoded)[600] - precise[40]) < .000001, "Render pasted audio from the copied source range");
|
|
102
|
+
var group = session.Copy(); group.Regions.Add(new TimelineRegion { Source = ramp.Source, Name = "Other", Track = 15, Start = 140, SourceStart = 5, Length = 30 });
|
|
103
|
+
var groupCopy = TimelineEdits.Copy(group, 0, true, 120, 180); TimelineEdits.Paste(group, groupCopy, 7, 500);
|
|
104
|
+
Check(group.Regions.Any(r => r.Track == 0 && r.Start == 500) && group.Regions.Any(r => r.Track == 15 && r.Start == 520),
|
|
105
|
+
"Copy all-track selections with original track numbers and relative timing");
|
|
106
|
+
var selectedOnly = group.Copy(); TimelineEdits.Delete(selectedOnly, 0, false, 0, 1000);
|
|
107
|
+
Check(selectedOnly.Regions.All(r => r.Track == 15), "Limit selected-track cuts to that track");
|
|
108
|
+
var invalid = session.Copy(); invalid.Regions[0].Track = 16; bool blocked = false;
|
|
109
|
+
try { invalid.Validate(); } catch (IOException) { blocked = true; }
|
|
110
|
+
Check(blocked, "Reject a track beyond the 16-track editor limit");
|
|
111
|
+
string saved = Path.Combine(root, "Practice.dbedit"); group.Save(saved); var reopened = EditorSession.Load(saved);
|
|
112
|
+
Check(reopened.Tracks.Length == 16 && Render(reopened, decoded).SequenceEqual(Render(group, decoded)), "Save and reopen editor sessions without changing edited audio");
|
|
113
|
+
var clone = group.Copy(); clone.Regions[0].Length = 1; clone.Tracks[0].GainDb = -12;
|
|
114
|
+
Check(group.Regions[0].Length != 1 && group.Tracks[0].GainDb == 0, "Keep undo snapshots independent of later region and track edits");
|
|
115
|
+
bool protectedSource = false; try { group.Save(ramp.Source); } catch (IOException) { protectedSource = true; }
|
|
116
|
+
Check(protectedSource && original.SequenceEqual(File.ReadAllBytes(ramp.Source)), "Refuse to save a session over original audio");
|
|
117
|
+
|
|
118
|
+
Clip left = Source(root, "Kick", Enumerable.Repeat(.6f, Audio.Rate).ToArray(), 1);
|
|
119
|
+
Clip right = Source(root, "Snare", Enumerable.Repeat(.7f, Audio.Rate).ToArray(), 1);
|
|
120
|
+
var mixedAssets = new Dictionary<string, Clip>(StringComparer.OrdinalIgnoreCase) { { left.Source, left }, { right.Source, right } };
|
|
121
|
+
var mixState = new EditorSession { Name = "Practice mix" };
|
|
122
|
+
mixState.Regions.Add(new TimelineRegion { Source = left.Source, Name = "Kick", Track = 0, Start = 0, Length = Audio.Rate });
|
|
123
|
+
mixState.Regions.Add(new TimelineRegion { Source = right.Source, Name = "Snare", Track = 1, Start = 0, Length = Audio.Rate });
|
|
124
|
+
Check(Render(mixState, mixedAssets).All(v => Math.Abs(v - 1.3f) < .00001), "Mix simultaneous tracks in float without clipping over-0-dBFS samples");
|
|
125
|
+
using (var mix = new TimelineMix(mixState, mixedAssets)) {
|
|
126
|
+
var listening = AudioOutputs.Adapt(mix, 48000, 2); var samples = new float[400]; int count = listening.Read(samples, 0, samples.Length);
|
|
127
|
+
Check(listening.WaveFormat.SampleRate == 48000 && count == 400 && samples.All(v => Math.Abs(v) <= 1),
|
|
128
|
+
"Adapt the float editor mix to the endpoint rate and bound its listening samples");
|
|
129
|
+
}
|
|
130
|
+
var solo = mixState.Copy(); solo.Tracks[1].Solo = true;
|
|
131
|
+
Check(Render(solo, mixedAssets).All(v => Math.Abs(v - .7f) < .00001), "Solo isolates the chosen editor track");
|
|
132
|
+
solo.Tracks[1].Mute = true; Check(Render(solo, mixedAssets).All(v => v == 0), "Mute silences a soloed track without leaking other tracks");
|
|
133
|
+
var level = mixState.Copy(); level.Tracks[0].GainDb = -6; level.Tracks[1].Mute = true;
|
|
134
|
+
Check(Math.Abs(Render(level, mixedAssets)[0] - .6 * Math.Pow(10, -.3)) < .00001, "Apply track level changes in the timeline mix");
|
|
135
|
+
foreach (int bits in new[] { 16, 24, 32 }) {
|
|
136
|
+
string path = Path.Combine(root, "editor-mix-" + bits + ".wav");
|
|
137
|
+
TimelineRender.Export(mixState, mixedAssets, path, bits, true, 0, 0, CancellationToken.None, null);
|
|
138
|
+
Clip exported = Audio.Load(path, Path.Combine(root, "cache"), CancellationToken.None, null);
|
|
139
|
+
using (var reader = new WaveFileReader(path)) Check(reader.WaveFormat.BitsPerSample == bits && reader.WaveFormat.SampleRate == Audio.Rate && reader.Length == Audio.Rate * 2 * bits / 8,
|
|
140
|
+
"Export exact-duration " + bits + "-bit stereo WAV from the multitrack editor");
|
|
141
|
+
Check(bits == 32 ? exported.Peak > 1.29 : Math.Abs(exported.PeakDb + 1) < .02,
|
|
142
|
+
bits == 32 ? "Preserve float mix headroom" : "Scale the full PCM mix to prevent clipping");
|
|
143
|
+
}
|
|
144
|
+
string selectedMix = Path.Combine(root, "selected-mix.wav");
|
|
145
|
+
TimelineRender.Export(mixState, mixedAssets, selectedMix, 32, false, Audio.Rate / 4, Audio.Rate / 2, CancellationToken.None, null);
|
|
146
|
+
using (var reader = new WaveFileReader(selectedMix)) Check(reader.Length == Audio.Rate / 4 * 8, "Export only the selected timeline range");
|
|
147
|
+
var cancelled = new CancellationTokenSource(); cancelled.Cancel(); string cancelledPath = Path.Combine(root, "cancelled.wav"); bool wasCancelled = false;
|
|
148
|
+
try { TimelineRender.Export(mixState, mixedAssets, cancelledPath, 32, false, 0, 0, cancelled.Token, null); } catch (OperationCanceledException) { wasCancelled = true; }
|
|
149
|
+
Check(wasCancelled && !File.Exists(cancelledPath) && !Directory.GetFiles(root, "*.partial").Any(), "Cancel rendering without leaving incomplete exports");
|
|
150
|
+
Check(original.SequenceEqual(File.ReadAllBytes(ramp.Source)), "Keep original source bytes unchanged through all timeline edits");
|
|
151
|
+
|
|
152
|
+
foreach (string codec in new[] { "pcm_s16le", "pcm_s24le", "pcm_s32le", "pcm_f32le", "pcm_f64le" }) {
|
|
153
|
+
string file = Path.Combine(root, "player-" + codec + ".wav");
|
|
154
|
+
Audio.Run("-hide_banner -v error -nostdin -y -i " + Audio.Q(left.Source) + " -ar 48000 -c:a " + codec + " " + Audio.Q(file), CancellationToken.None, null, true);
|
|
155
|
+
var clip = Audio.Load(file, Path.Combine(root, "cache"), CancellationToken.None, null);
|
|
156
|
+
Check(clip.Ready && Math.Abs(clip.Duration - 1) < .001 && clip.Peak > .59 && clip.SourceFormat.Contains("48000 Hz"),
|
|
157
|
+
"Decode " + codec + " at 48 kHz for the Media Player and display source format");
|
|
158
|
+
}
|
|
159
|
+
string aiff = Path.Combine(root, "player-pcm-big-endian.aiff");
|
|
160
|
+
Audio.Run("-hide_banner -v error -nostdin -y -i " + Audio.Q(left.Source) + " -c:a pcm_s24be " + Audio.Q(aiff), CancellationToken.None, null, true);
|
|
161
|
+
Check(Audio.Load(aiff, Path.Combine(root, "cache"), CancellationToken.None, null).Ready, "Decode big-endian PCM AIFF without Windows Media Player codecs");
|
|
162
|
+
|
|
163
|
+
Window owner;
|
|
164
|
+
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Main.xaml")) owner = (Window)XamlReader.Load(stream);
|
|
165
|
+
ThemePalette.Install(owner);
|
|
166
|
+
Check(owner.FindName("AudioDevicesButton") != null, "Expose Audio devices in the main header on every workspace");
|
|
167
|
+
Picture((FrameworkElement)owner.Content, Path.Combine(root, "main-header-preview.png"), 1050, 850);
|
|
168
|
+
using (var devices = new AudioDevicesView(AudioInputs.Choices().FirstOrDefault(), delegate(InputChoice choice) { }, delegate { }, false)) {
|
|
169
|
+
owner.Content = devices; Picture(devices, Path.Combine(root, "audio-devices-preview.png"), 760, 640);
|
|
170
|
+
Check(devices.ActualWidth == 712 && devices.DesiredSize.Height <= 640, "Render output selector, speaker test, microphone selector and diagnostics together");
|
|
171
|
+
}
|
|
172
|
+
using (var view = new EditorView(owner, Path.Combine(root, "editor-cache"), delegate(string message) { }, delegate { })) {
|
|
173
|
+
owner.Content = view; view.SetForTest(mixState, mixedAssets);
|
|
174
|
+
Check(view.CheckUndoRedoForTest(), "Exercise the editor's actual cut, undo and redo handlers");
|
|
175
|
+
float[] pulse = new float[Audio.Rate * 8];
|
|
176
|
+
for (int i = 0; i < pulse.Length; i++) pulse[i] = (float)(.7 * Math.Exp(-(i % (Audio.Rate / 2)) / 3000.0) * Math.Sin(i * .06));
|
|
177
|
+
var beat = Source(root, "Generated drum reference", pulse, 1);
|
|
178
|
+
var previewAssets = new Dictionary<string, Clip>(StringComparer.OrdinalIgnoreCase) { { beat.Source, beat } };
|
|
179
|
+
var previewState = new EditorSession { Name = "Practice · generated test audio" };
|
|
180
|
+
string[] names = { "Kick", "Snare", "Hi-hat", "Tom 1", "Tom 2", "Floor tom", "Overhead L", "Overhead R", "Room L", "Room R", "Percussion", "Bass guide", "Guitar guide", "Keys guide", "Click", "Reference" };
|
|
181
|
+
for (int i = 0; i < 16; i++) { previewState.Tracks[i].Name = names[i]; previewState.Regions.Add(new TimelineRegion { Source = beat.Source, Name = names[i], Track = i, Start = (i % 3) * Audio.Rate / 2, SourceStart = i % 2 * Audio.Rate, Length = (6 - i % 3) * Audio.Rate }); }
|
|
182
|
+
view.SetForTest(previewState, previewAssets); view.ShowSelectionForTest(Audio.Rate, 4L * Audio.Rate); Picture(view, Path.Combine(root, "editor-preview.png"), 1280, 760);
|
|
183
|
+
Picture(view, Path.Combine(root, "editor-small-preview.png"), 800, 560);
|
|
184
|
+
Check(view.ActualWidth == 800 && view.ActualHeight == 560, "Lay out editor controls and scrolling timeline at small window size");
|
|
185
|
+
}
|
|
186
|
+
using (var view = new MediaPlayerView(owner, Path.Combine(root, "player-cache"), delegate(string message) { }, delegate { })) {
|
|
187
|
+
owner.Content = view; view.SetForTest(left); Picture(view, Path.Combine(root, "player-preview.png"), 1100, 760);
|
|
188
|
+
Picture(view, Path.Combine(root, "player-small-preview.png"), 800, 560);
|
|
189
|
+
Check(view.ActualWidth == 800, "Render the Media Player waveform, playlist, transport and seek controls");
|
|
190
|
+
}
|
|
191
|
+
owner.Content = null;
|
|
192
|
+
log.Add("Rendered views use generated samples. Realtek playback on the affected PC and physical Windows/H5studio capture need a connected-device acceptance test.");
|
|
193
|
+
File.WriteAllLines(Path.Combine(root, "workspace-test-results.txt"), log, Encoding.UTF8); return 0;
|
|
194
|
+
}
|
|
195
|
+
catch (Exception ex) { log.Add("FAIL " + ex); File.WriteAllLines(Path.Combine(root, "workspace-test-results.txt"), log, Encoding.UTF8); return 1; }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
|
3
|
+
<assemblyIdentity version="1.0.0.0" name="DrumBuddy"/>
|
|
4
|
+
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"><security><requestedPrivileges><requestedExecutionLevel level="asInvoker" uiAccess="false"/></requestedPrivileges></security></trustInfo>
|
|
5
|
+
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"><application><supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/></application></compatibility>
|
|
6
|
+
<application xmlns="urn:schemas-microsoft-com:asm.v3"><windowsSettings><dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware><dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness><longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware></windowsSettings></application>
|
|
7
|
+
</assembly>
|
|
Binary file
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"File": "naudio.wasapi.zip",
|
|
4
|
+
"Url": "https://api.nuget.org/v3-flatcontainer/naudio.wasapi/2.2.1/naudio.wasapi.2.2.1.nupkg",
|
|
5
|
+
"ExtractTo": "naudio.wasapi",
|
|
6
|
+
"Sha256": "C8396B8DBAB86A2619EDFD9158BEAF18C21CB86677099B5594950FD8651B9D1A"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"File": "naudio.core.zip",
|
|
10
|
+
"Url": "https://api.nuget.org/v3-flatcontainer/naudio.core/2.2.1/naudio.core.2.2.1.nupkg",
|
|
11
|
+
"ExtractTo": "naudio.core",
|
|
12
|
+
"Sha256": "794645DBFD30E4880663D52D9D9224D55C301FA54228FF623C95D572C7887347"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"File": "naudio.asio.zip",
|
|
16
|
+
"Url": "https://api.nuget.org/v3-flatcontainer/naudio.asio/2.2.1/naudio.asio.2.2.1.nupkg",
|
|
17
|
+
"ExtractTo": "naudio.asio",
|
|
18
|
+
"Sha256": "AAE0C06C2559FBF56BD4B4F4A5CB9AEEF158E35C7B536699E4E6B8012A683577"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"File": "naudio.winmm.zip",
|
|
22
|
+
"Url": "https://api.nuget.org/v3-flatcontainer/naudio.winmm/2.2.1/naudio.winmm.2.2.1.nupkg",
|
|
23
|
+
"ExtractTo": "naudio.winmm",
|
|
24
|
+
"Sha256": "48DA978D715F219489C050EE8C32C64F80FF73718EBA237EBCBC636BA5902BE3"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"File": "microsoft.win32.registry.zip",
|
|
28
|
+
"Url": "https://api.nuget.org/v3-flatcontainer/microsoft.win32.registry/4.7.0/microsoft.win32.registry.4.7.0.nupkg",
|
|
29
|
+
"ExtractTo": "microsoft.win32.registry",
|
|
30
|
+
"Sha256": "FA3582C11A94FC9FE32DD40A0C59BDDD67C80EB0CC5CC27DF3851EBDA40CA22F"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"File": "system.security.accesscontrol.zip",
|
|
34
|
+
"Url": "https://api.nuget.org/v3-flatcontainer/system.security.accesscontrol/4.7.0/system.security.accesscontrol.4.7.0.nupkg",
|
|
35
|
+
"ExtractTo": "system.security.accesscontrol",
|
|
36
|
+
"Sha256": "FFD6423C81CB761CEAECE5B850AA93B11D0EF778E31DDE81446D524708071358"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"File": "system.security.principal.windows.zip",
|
|
40
|
+
"Url": "https://api.nuget.org/v3-flatcontainer/system.security.principal.windows/4.7.0/system.security.principal.windows.4.7.0.nupkg",
|
|
41
|
+
"ExtractTo": "system.security.principal.windows",
|
|
42
|
+
"Sha256": "AD604CD94F0AAB7AC475A6B530F65260E3A46ED306816C81F223EBA48AA6A6A8"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"File": "ffmpeg.zip",
|
|
46
|
+
"Url": "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-9.0.2-essentials_build.zip",
|
|
47
|
+
"ExtractTo": "ffmpeg",
|
|
48
|
+
"Sha256": "60F467265B1E312373DBCD92200C2618A74850F98D3D078E94296BB3FA2047BA"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"File": "NAudio-license.txt",
|
|
52
|
+
"Url": "https://raw.githubusercontent.com/naudio/NAudio/v2.2.1/license.txt",
|
|
53
|
+
"ExtractTo": null,
|
|
54
|
+
"Sha256": "303E01786B271EB464B3F43D18A375F2F17D37BC708A37C2FDF45B289FB5BFAA"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"File": "r16-panel.png",
|
|
58
|
+
"Url": "https://zoomcorp.com/media/original_images/R16_list_0.png",
|
|
59
|
+
"ExtractTo": null,
|
|
60
|
+
"Sha256": "5564FA622F101DC27EA402C399C5B9B1E1E039B8771DAF40B185DAB9772ADF38"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
Binary file
|
package/source/logo.png
ADDED
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
The independent C# R16 project container reader/writer uses file-format facts
|
|
2
|
+
researched with zoomrlib 1.1.0 by Remy Taymans and Glenn Boysko:
|
|
3
|
+
https://gitlab.com/remytms/zoomrlib
|
|
4
|
+
https://pypi.org/project/zoomrlib/1.1.0/
|
|
5
|
+
|
|
6
|
+
Its unchanged binary fixture projects were used for round-trip verification.
|
|
7
|
+
The upstream Python implementation is not bundled or linked into Drum Buddy.
|
|
8
|
+
Download the upstream release's tests/data to reproduce the native fixture tests:
|
|
9
|
+
DrumBuddy.exe --control-self-test TEST_OUTPUT PATH_TO_TEST_FIXTURES
|
|
10
|
+
|
|
11
|
+
Mackie control protocol reference (message assignments only):
|
|
12
|
+
https://github.com/justinfrankel/reaper-sdk/blob/main/reaper-plugins/reaper_csurf/csurf_mcu.cpp
|
|
13
|
+
|
|
14
|
+
Official R16 manuals:
|
|
15
|
+
https://zoomcorp.com/media/documents/E_R16_.pdf
|
|
16
|
+
https://zoomcorp.com/media/documents/E_R16_AIF_.pdf
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs/promises');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const { execFileSync } = require('node:child_process');
|
|
9
|
+
const { hash, cached, installRoot } = require('../scripts/install');
|
|
10
|
+
const pkg = require('../package.json');
|
|
11
|
+
const dependencies = require('../dependencies.json');
|
|
12
|
+
test('launcher identifies the packaged release and update command', () => {
|
|
13
|
+
assert.equal(pkg.name, '@pro-gram/flux');
|
|
14
|
+
assert.equal(pkg.bin.flux, 'bin/flux.js');
|
|
15
|
+
const launcher = path.resolve(__dirname, '../bin/flux.js');
|
|
16
|
+
assert.equal(execFileSync(process.execPath, [launcher, '--version'], { encoding: 'utf8' }).trim(), pkg.version);
|
|
17
|
+
const help = execFileSync(process.execPath, [launcher, '--help'], { encoding: 'utf8' });
|
|
18
|
+
assert.match(help, /npm\.cmd install -g @pro-gram\/flux@latest/);
|
|
19
|
+
assert.match(help, /32-bit float/);
|
|
20
|
+
assert.match(help, /16\/24-bit PCM/);
|
|
21
|
+
assert.match(help, /16-track timeline Editor/);
|
|
22
|
+
assert.match(help, /PCM\/float Media Player/);
|
|
23
|
+
assert.match(help, /--audio-diagnostics/);
|
|
24
|
+
assert.match(help, /Backspace/);
|
|
25
|
+
assert.match(help, /1\/256/);
|
|
26
|
+
assert.doesNotMatch(help, /--allow-scripts/);
|
|
27
|
+
});
|
|
28
|
+
test('an older Flux launcher preserves a newer installed release', () => {
|
|
29
|
+
const { needsUpdate } = require('../scripts/version');
|
|
30
|
+
assert.equal(needsUpdate('1.4.1', '1.5.0'), true);
|
|
31
|
+
assert.equal(needsUpdate('1.5.0\n', '1.5.0'), false);
|
|
32
|
+
assert.equal(needsUpdate('1.6.0', '1.5.0'), false);
|
|
33
|
+
assert.equal(needsUpdate(null, '1.5.0'), true);
|
|
34
|
+
assert.equal(needsUpdate('bad marker', '1.5.0'), true);
|
|
35
|
+
});
|
|
36
|
+
test('public installer matches its package integrity pin', async () => {
|
|
37
|
+
assert.equal(await hash(path.resolve(__dirname, '../payload/DrumBuddy-Setup.exe')), dependencies.installerSha256);
|
|
38
|
+
});
|
|
39
|
+
test('verified cached data works without a network call', async () => {
|
|
40
|
+
const folder = await fs.mkdtemp(path.join(os.tmpdir(), 'drumbuddy-cache-check-'));
|
|
41
|
+
const contents = Buffer.from('Drum Buddy cache test');
|
|
42
|
+
const file = path.join(folder, 'fixture');
|
|
43
|
+
await fs.writeFile(file, contents);
|
|
44
|
+
const asset = { file: 'fixture', url: 'https://invalid.example/', sha256: crypto.createHash('sha256').update(contents).digest('hex') };
|
|
45
|
+
assert.equal(await cached(asset, folder), file);
|
|
46
|
+
assert.equal(await hash(file), asset.sha256);
|
|
47
|
+
assert.equal(await cached({ ...asset, sha256: '0'.repeat(64), alternateSha256: asset.sha256 }, folder), file);
|
|
48
|
+
});
|
|
49
|
+
test('dependencies are pinned to official HTTPS sources and exclude driver downloads', () => {
|
|
50
|
+
assert.equal(new URL(dependencies.ffmpeg.url).host, 'www.gyan.dev');
|
|
51
|
+
assert.equal(new URL(dependencies.panel.url).host, 'zoomcorp.com');
|
|
52
|
+
assert.match(dependencies.ffmpeg.sha256, /^[a-f0-9]{64}$/);
|
|
53
|
+
assert.equal(dependencies.driver, undefined);
|
|
54
|
+
assert.equal(installRoot(), path.join(process.env.LOCALAPPDATA, 'Programs', 'DrumBuddy'));
|
|
55
|
+
});
|