@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.
Files changed (79) hide show
  1. package/LICENSE +23 -0
  2. package/NOTICE.txt +33 -0
  3. package/README.md +125 -2
  4. package/bin/flux.js +29 -0
  5. package/dependencies.json +15 -0
  6. package/package.json +60 -6
  7. package/payload/DrumBuddy-Setup.exe +4 -0
  8. package/scripts/install.js +60 -0
  9. package/scripts/version.js +12 -0
  10. package/source/AssetBootstrap.cs +49 -0
  11. package/source/Audio Decoders.txt +230 -0
  12. package/source/Audio.cs +453 -0
  13. package/source/AudioDeviceSettings.cs +126 -0
  14. package/source/AudioEndpointWatcher.cs +25 -0
  15. package/source/AudioInputs.cs +50 -0
  16. package/source/AudioOutputs.cs +307 -0
  17. package/source/AudioWorkspace.cs +72 -0
  18. package/source/BrandArtwork.cs +71 -0
  19. package/source/Build Instructions.txt +65 -0
  20. package/source/Build.ps1 +135 -0
  21. package/source/ControlMode.cs +687 -0
  22. package/source/ControlTests.cs +88 -0
  23. package/source/ConversionFormats.cs +59 -0
  24. package/source/ConversionTests.cs +66 -0
  25. package/source/DockablePane.cs +95 -0
  26. package/source/DriverSetup.cs +28 -0
  27. package/source/EditorCompactLayout.cs +36 -0
  28. package/source/EditorPlaybackTests.cs +87 -0
  29. package/source/EditorSelectionTests.cs +73 -0
  30. package/source/EditorToolInteractions.cs +227 -0
  31. package/source/EditorTools.cs +78 -0
  32. package/source/EditorToolsTests.cs +93 -0
  33. package/source/EditorView.cs +798 -0
  34. package/source/Fetch-Dependencies.ps1 +15 -0
  35. package/source/FloatRecording.cs +99 -0
  36. package/source/FloatRecordingTests.cs +170 -0
  37. package/source/Flux-Logo.png +0 -0
  38. package/source/Flux-Mark.svg +49 -0
  39. package/source/FluxTempo Logo Prompt.txt +3 -0
  40. package/source/FluxTempo-Mark.svg +49 -0
  41. package/source/HomeView.cs +32 -0
  42. package/source/InterfaceSettings.cs +225 -0
  43. package/source/InterfaceTests.cs +172 -0
  44. package/source/LICENSE.txt +23 -0
  45. package/source/Logo Prompt.txt +16 -0
  46. package/source/Main.cs +787 -0
  47. package/source/Main.xaml +289 -0
  48. package/source/MediaPlayerView.cs +274 -0
  49. package/source/MidiControl.cs +113 -0
  50. package/source/MidiPerformance.cs +125 -0
  51. package/source/Mixer.cs +280 -0
  52. package/source/NativeMemory.cs +165 -0
  53. package/source/PanelFeedback.cs +75 -0
  54. package/source/PlaybackTests.cs +141 -0
  55. package/source/Quick Start.txt +96 -0
  56. package/source/SelfTest.cs +78 -0
  57. package/source/SettingsWindow.cs +209 -0
  58. package/source/Setup.cs +196 -0
  59. package/source/Setup.xaml +28 -0
  60. package/source/ShellStyles.xaml +99 -0
  61. package/source/TempoGrid.cs +95 -0
  62. package/source/TempoGridTests.cs +108 -0
  63. package/source/Third Party Notices.txt +33 -0
  64. package/source/Timeline.cs +268 -0
  65. package/source/TimelineGestures.cs +57 -0
  66. package/source/TimelineZoom.cs +21 -0
  67. package/source/Uninstall.cs +69 -0
  68. package/source/WaveformDetail.cs +104 -0
  69. package/source/WindowsAudioPlayer.cs +137 -0
  70. package/source/WorkspaceTests.cs +202 -0
  71. package/source/ZoomTests.cs +95 -0
  72. package/source/app.manifest +7 -0
  73. package/source/brand-archive/Drum-Buddy-Black-Hair.png +0 -0
  74. package/source/dependency-lock.json +62 -0
  75. package/source/fluxtempo-mark.png +0 -0
  76. package/source/logo.png +0 -0
  77. package/source/r16-panel.png +0 -0
  78. package/source/references/R16 Format Research.txt +16 -0
  79. package/test/package.test.js +55 -0
@@ -0,0 +1,687 @@
1
+ using System;
2
+ using System.IO;
3
+ using System.Linq;
4
+ using System.Collections.Generic;
5
+ using System.Globalization;
6
+ using System.Threading;
7
+ using System.Threading.Tasks;
8
+ using System.Xml.Serialization;
9
+ using System.Windows;
10
+ using System.Windows.Controls;
11
+ using System.Windows.Controls.Primitives;
12
+ using System.Windows.Automation;
13
+ using System.Windows.Input;
14
+ using System.Windows.Media;
15
+ using System.Windows.Media.Imaging;
16
+ using System.Windows.Shapes;
17
+ using Path = System.IO.Path;
18
+ using System.Windows.Threading;
19
+ using NAudio.Wave;
20
+
21
+ namespace DrumBuddy
22
+ {
23
+ // Native sliders retain keyboard/range accessibility while drawing the physical controls.
24
+ internal class PanelFader : Slider
25
+ {
26
+ public bool IsMaster;
27
+ public PanelFader() { Minimum=0;Maximum=127;Value=100;Focusable=true;SmallChange=1;LargeChange=10;IsSnapToTickEnabled=true;TickFrequency=1;Template=new ControlTemplate(typeof(Slider)); }
28
+ protected override void OnValueChanged(double oldValue,double newValue) { base.OnValueChanged(oldValue,newValue);InvalidateVisual(); }
29
+ protected override void OnRender(DrawingContext dc)
30
+ {
31
+ double w=ActualWidth,h=ActualHeight;
32
+ dc.DrawRectangle(new LinearGradientBrush(Color.FromRgb(224,227,229),Color.FromRgb(246,247,248),0),null,new Rect(0,0,w,h));
33
+ dc.DrawRoundedRectangle(Brushes.Black,new Pen(Brushes.Gray,1),new Rect(w/2-6,4,12,h-8),2,2);
34
+ double y=8+(1-Value/127)*(h-70);
35
+ var gradient=new LinearGradientBrush(IsMaster?Color.FromRgb(211,75,61):Color.FromRgb(232,234,232),IsMaster?Color.FromRgb(134,43,38):Color.FromRgb(151,157,153),90);
36
+ dc.DrawRoundedRectangle(gradient,new Pen(Brushes.Gray,1),new Rect(1,y,w-2,56),2,2);
37
+ for(int i=5;i<53;i+=4) dc.DrawLine(new Pen(new SolidColorBrush(Color.FromArgb(75,45,55,50)),1),new Point(3,y+i),new Point(w-3,y+i));
38
+ dc.DrawLine(new Pen(IsMaster?Brushes.White:Brushes.DimGray,2),new Point(2,y+28),new Point(w-2,y+28));
39
+ if(IsKeyboardFocused) dc.DrawRectangle(null,new Pen(Brushes.Teal,2),new Rect(1,1,w-2,h-2));
40
+ }
41
+ private void PointValue(Point p) { Value=Math.Max(0,Math.Min(127,(1-(p.Y-36)/(ActualHeight-70))*127)); }
42
+ protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { Focus();CaptureMouse();PointValue(e.GetPosition(this));e.Handled=true; }
43
+ protected override void OnMouseMove(MouseEventArgs e) { if(IsMouseCaptured) PointValue(e.GetPosition(this)); }
44
+ protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) { if(IsMouseCaptured) ReleaseMouseCapture();e.Handled=true; }
45
+ protected override void OnMouseWheel(MouseWheelEventArgs e) { Value+=e.Delta>0?1:-1;e.Handled=true; }
46
+ }
47
+ internal class PanelDial : Slider
48
+ {
49
+ private Point start;
50
+ private double startValue;
51
+ public PanelDial() { Minimum=-50;Maximum=50;Focusable=true;SmallChange=1;IsSnapToTickEnabled=true;TickFrequency=1;Template=new ControlTemplate(typeof(Slider)); }
52
+ protected override void OnValueChanged(double a,double b) { base.OnValueChanged(a,b);InvalidateVisual(); }
53
+ protected override void OnRender(DrawingContext dc)
54
+ {
55
+ double r=Math.Min(ActualWidth,ActualHeight)/2-3,cx=ActualWidth/2,cy=ActualHeight/2;
56
+ dc.DrawEllipse(new LinearGradientBrush(Color.FromRgb(214,219,216),Color.FromRgb(154,162,156),45),new Pen(Brushes.Gray,2),new Point(cx,cy),r,r);
57
+ double angle=(-135+270*(Value-Minimum)/(Maximum-Minimum))*Math.PI/180;
58
+ dc.DrawLine(new Pen(Brushes.DarkSlateGray,3),new Point(cx+Math.Sin(angle)*r*.5,cy-Math.Cos(angle)*r*.5),new Point(cx+Math.Sin(angle)*r*.85,cy-Math.Cos(angle)*r*.85));
59
+ if(IsKeyboardFocused) dc.DrawEllipse(null,new Pen(Brushes.Teal,2),new Point(cx,cy),r+2,r+2);
60
+ }
61
+ protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { Focus();CaptureMouse();start=e.GetPosition(this);startValue=Value;e.Handled=true; }
62
+ protected override void OnMouseMove(MouseEventArgs e) { if(IsMouseCaptured) { Point p=e.GetPosition(this);Value=startValue+(start.Y-p.Y+p.X-start.X)*(Maximum-Minimum)/160; } }
63
+ protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) { if(IsMouseCaptured) ReleaseMouseCapture();e.Handled=true; }
64
+ protected override void OnMouseWheel(MouseWheelEventArgs e) { Value+=(e.Delta>0?1:-1)*SmallChange;e.Handled=true; }
65
+ }
66
+
67
+ public sealed class ControlMode : IDisposable
68
+ {
69
+ private readonly MixerEngine mixer=new MixerEngine();
70
+ private readonly ControlAudio audio=new ControlAudio();
71
+ private readonly R16Midi midi=new R16Midi();
72
+ private readonly MidiPerformance performance=new MidiPerformance();
73
+ private Window window,home;
74
+ private Action returnHome;
75
+ private Canvas panel;
76
+ private Border sidebar;
77
+ private Button menuButton;
78
+ private TextBlock notice,lcdTop,lcdBottom,deviceText,projectText,fxText,trackText,noteStatus;
79
+ private readonly PanelFader[] faders=new PanelFader[9];
80
+ private readonly PanelMeter[] mainMeters=new PanelMeter[10];
81
+ private readonly PanelLamp[] statusLeds=new PanelLamp[8],inputClipLeds=new PanelLamp[8],bankLeds=new PanelLamp[2];
82
+ private readonly Button[] statusButtons=new Button[8];
83
+ private PanelLamp modeLamp;
84
+ private Button playButton,recordButton;
85
+ private readonly MeterBallistics[] trackLevels=Enumerable.Range(0,16).Select(i=>new MeterBallistics()).ToArray();
86
+ private readonly double[] inputClipHold=new double[8];
87
+ private readonly TextBlock[] overviewText=new TextBlock[16];
88
+ private readonly Button[] overview=new Button[16];
89
+ private readonly ProgressBar[] overviewMeters=new ProgressBar[16];
90
+ private PanelDial jog;
91
+ private Slider pan,high,mid,low,reverb,chorus;
92
+ private ComboBox highFrequency,midFrequency,lowFrequency,midQ,outputDevice,midiInput,midiOutput,projectList,meterSource,noteInput,noteOutput;
93
+ private CheckBox mute,solo,arm,invert,stereo,highOn,midOn,lowOn,reverbOn,chorusOn,noteArm;
94
+ private RadioButton bits16,bits24;
95
+ private TextBox projectName;
96
+ private ListBox memoryFiles;
97
+ private readonly List<string> projectFolders=new List<string>();
98
+ private readonly List<double> markers=new List<double>();
99
+ private readonly Stack<MixerSession> undo=new Stack<MixerSession>();
100
+ private readonly DispatcherTimer timer=new DispatcherTimer { Interval=TimeSpan.FromMilliseconds(75) };
101
+ private R16Project native;
102
+ private string song="NEW SONG",recordFolder,exportFolder,library,cache,sessionPath,recoveryFile,takeFolder;
103
+ private bool updating,busy,recording,finishing,leaving,disposed,dirty,leaveAfterWork,lastPlaying,lastRecording,restoreAttempted;
104
+ private DateTime lastRecovery;
105
+ private int selected,bank,statusMode=0,bits=24;
106
+ private int[] recordedTracks;
107
+ private double recordStart;
108
+ private CancellationTokenSource cancellation;
109
+ private int parameter;
110
+ private string temporaryLcd;
111
+ private DateTime lcdUntil;
112
+ private readonly CultureInfo inv=CultureInfo.InvariantCulture;
113
+ private static Brush B(string value) { return (Brush)new BrushConverter().ConvertFromString(value); }
114
+ public ControlMode(string recordings,string exports)
115
+ {
116
+ recordFolder=recordings;exportFolder=exports;
117
+ string music=Environment.GetFolderPath(Environment.SpecialFolder.MyMusic);
118
+ if(string.IsNullOrEmpty(music)) music=Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
119
+ library=Path.Combine(music,"Drum Buddy","Projects");
120
+ cache=Path.Combine(Path.GetTempPath(),"DrumBuddyControl",Guid.NewGuid().ToString("N"));
121
+ recoveryFile=Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"DrumBuddy","control-session.dbuddy");
122
+ timer.Tick+=async delegate { await Tick(); };
123
+ midi.Received+=delegate(McuAction action) {
124
+ var target=window;
125
+ if(target!=null) target.Dispatcher.BeginInvoke((Action)delegate { if(window==target && target.IsVisible && !busy && !leaving) Midi(action); });
126
+ };
127
+ }
128
+ public async void Open(Window owner,Action done)
129
+ {
130
+ if(disposed) return;
131
+ home=owner;returnHome=done;
132
+ if(window==null) BuildWindow();
133
+ leaving=false;window.Owner=owner;home.Hide();window.Show();window.Activate();
134
+ RefreshDevices();TryMidi();
135
+ if(!restoreAttempted) { restoreAttempted=true;if(File.Exists(recoveryFile))await LoadSession(recoveryFile,true); }
136
+ EnsureAudio();Sync();timer.Start();
137
+ }
138
+ private void BuildWindow()
139
+ {
140
+ window=new Window { Title="Drum Buddy — R16 Full Control",WindowStyle=WindowStyle.None,ResizeMode=ResizeMode.NoResize,WindowState=WindowState.Maximized,
141
+ Background=B("#20272B"),Foreground=B("#E8EDEC"),FontFamily=new FontFamily("Segoe UI"),FontSize=15,AllowDrop=true };
142
+ var root=new Grid();window.Content=root;
143
+ var body=new DockPanel { Margin=new Thickness(12,4,12,8) };root.Children.Add(body);
144
+ var header=new Grid { Height=34 };header.ColumnDefinitions.Add(new ColumnDefinition { Width=GridLength.Auto });header.ColumnDefinitions.Add(new ColumnDefinition());header.ColumnDefinitions.Add(new ColumnDefinition { Width=GridLength.Auto });
145
+ DockPanel.SetDock(header,Dock.Top);body.Children.Add(header);
146
+ menuButton=TextButton("Open Menu",delegate { ToggleMenu(); });menuButton.FontSize=13;menuButton.FontWeight=FontWeights.Bold;header.Children.Add(menuButton);
147
+ deviceText=Text("R16 Full Control",13,"#BDC8CA");deviceText.VerticalAlignment=VerticalAlignment.Center;deviceText.HorizontalAlignment=HorizontalAlignment.Center;Grid.SetColumn(deviceText,1);header.Children.Add(deviceText);
148
+ var exit=TextButton("Esc · Home",async delegate { await Leave(); });Grid.SetColumn(exit,2);header.Children.Add(exit);
149
+ var footer=new StackPanel();DockPanel.SetDock(footer,Dock.Bottom);body.Children.Add(footer);
150
+ notice=Text("Drop audio onto a track. Open Menu for files, settings and USB.",14,"#C9D6D4");notice.TextWrapping=TextWrapping.Wrap;notice.Margin=new Thickness(6,2,6,5);footer.Children.Add(notice);
151
+ var tracks=new UniformGrid { Columns=16,Height=66 };footer.Children.Add(tracks);
152
+ for(int i=0;i<16;i++) {
153
+ int index=i;
154
+ var content=new StackPanel();
155
+ overviewText[i]=Text((i+1)+" · Empty",12,"#DCE7E5");overviewText[i].TextTrimming=TextTrimming.CharacterEllipsis;content.Children.Add(overviewText[i]);
156
+ overviewMeters[i]=new ProgressBar { Minimum=0,Maximum=1,Height=5,Margin=new Thickness(0,5,0,2),Foreground=B("#4FBD9C"),Background=B("#121A1D"),BorderThickness=new Thickness(0) };content.Children.Add(overviewMeters[i]);
157
+ overview[i]=new Button { Content=content,Margin=new Thickness(2),Padding=new Thickness(6),Background=B("#303C41"),BorderBrush=B("#4B5A60"),BorderThickness=new Thickness(1),ToolTip="Track "+(i+1)+" · click to select its fader bank",AllowDrop=true };
158
+ AutomationProperties.SetName(overview[i],"Select track "+(i+1));overview[i].Click+=delegate { Select(index); };
159
+ overview[i].Drop+=async delegate(object sender,DragEventArgs e) { if(e.Data.GetDataPresent(DataFormats.FileDrop)) { e.Handled=true;await AddAudio((string[])e.Data.GetData(DataFormats.FileDrop),index); } };
160
+ tracks.Children.Add(overview[i]);
161
+ }
162
+ panel=new Canvas { Width=1340,Height=830,Background=Brushes.White,ClipToBounds=true };
163
+ var image=new BitmapImage(new Uri(Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"r16-panel.png")));
164
+ var photo=new Image { Source=new CroppedBitmap(image,new Int32Rect(80,155,1340,830)),Width=1340,Height=830,IsHitTestVisible=false };
165
+ panel.Children.Add(photo);
166
+ var view=new Viewbox { Stretch=Stretch.Uniform,Child=panel,Margin=new Thickness(0,2,0,0) };body.Children.Add(view);
167
+ BuildPanel();
168
+ BuildSidebar(root);
169
+ window.PreviewKeyDown+=async delegate(object sender,KeyEventArgs e) {
170
+ if(e.Key==Key.Escape) { e.Handled=true;await Leave();return; }
171
+ if(Keyboard.Modifiers==ModifierKeys.Control && e.Key==Key.S) { e.Handled=true;SaveSession();return; }
172
+ if(Keyboard.Modifiers==ModifierKeys.Control && e.Key==Key.Z && !(Keyboard.FocusedElement is TextBox)) { e.Handled=true;Undo();return; }
173
+ if(Keyboard.FocusedElement is TextBox || Keyboard.FocusedElement is ComboBox) return;
174
+ if(e.Key==Key.Space) { e.Handled=true;if(recording) await StopTake();else PlayPause(); }
175
+ if(e.Key==Key.Home && !recording) { e.Handled=true;mixer.Seek(0); }
176
+ };
177
+ window.PreviewDragOver+=delegate(object sender,DragEventArgs e) { e.Effects=!busy && !recording && e.Data.GetDataPresent(DataFormats.FileDrop)?DragDropEffects.Copy:DragDropEffects.None;e.Handled=true; };
178
+ window.Drop+=async delegate(object sender,DragEventArgs e) { if(!e.Handled && e.Data.GetDataPresent(DataFormats.FileDrop)) { e.Handled=true;await AddAudio((string[])e.Data.GetData(DataFormats.FileDrop),selected); } };
179
+ window.Closing+=async delegate(object sender,System.ComponentModel.CancelEventArgs e) { if(!disposed) { e.Cancel=true;await Leave(); } };
180
+ }
181
+ private void BuildPanel()
182
+ {
183
+ int[] xs={207,294,381,469,556,644,731,818,906};
184
+ for(int i=0;i<8;i++) {
185
+ int index=i;
186
+ Hot(xs[i]-31,353,62,66,"Input "+(i+1)+" analogue gain (hardware)",delegate { Hardware("INPUT "+(index+1)+" GAIN","Turn the physical R16 gain knob. Analogue gain is not readable or adjustable over USB."); });
187
+ statusButtons[i]=Hot(xs[i]-16,628,32,32,"Track "+(i+1)+" / "+(i+9)+" status",delegate { ToggleStatus(bank+index); });
188
+ var led=new PanelLamp { IsHitTestVisible=false };At(led,xs[i]-39,633,20,20);statusLeds[i]=led;
189
+ var clip=new PanelLamp { IsHitTestVisible=false };At(clip,xs[i]-39,346,17,17);inputClipLeds[i]=clip;
190
+ }
191
+ for(int i=0;i<9;i++) {
192
+ int index=i;
193
+ var fader=new PanelFader { Width=40,Height=195,IsMaster=i==8,ToolTip=i==8?"PC master level":"PC track level" };
194
+ AutomationProperties.SetName(fader,i==8?"Master fader":"Fader "+(i+1));At(fader,xs[i]-20,699,40,195);faders[i]=fader;
195
+ fader.ValueChanged+=delegate { if(updating || busy) return;Remember();lock(mixer.Gate) { if(index==8) mixer.Master=(int)fader.Value;else { selected=bank+index;mixer.Tracks[selected].Fader=(int)fader.Value; } }dirty=true;Sync(false); };
196
+ fader.MouseDoubleClick+=delegate { fader.Value=100; };
197
+ }
198
+ // Cover photographed LEDs; every segment here is driven by audio samples.
199
+ At(new Border { Background=B("#050908"),IsHitTestVisible=false },170,451,774,93);
200
+ for(int i=0;i<10;i++) {
201
+ var meter=new PanelMeter { IsHitTestVisible=false,Compact=i>=8 };
202
+ At(meter,i<8?xs[i]-39:(i==8?879:911),453,i<8?68:26,88);mainMeters[i]=meter;
203
+ if(i<8)At(new Rectangle { Fill=B("#33443B"),IsHitTestVisible=false },xs[i]+42,455,1,86);
204
+ }
205
+ for(int i=0;i<2;i++) { bankLeds[i]=new PanelLamp { IsHitTestVisible=false };At(bankLeds[i],i==0?954:1033,382,19,19); }
206
+ modeLamp=new PanelLamp { IsHitTestVisible=false };At(modeLamp,866,635,18,18);
207
+ var lcd=new Border { Background=B("#F0A13C"),CornerRadius=new CornerRadius(3),Padding=new Thickness(6,3,6,2) };
208
+ At(lcd,1038,468,269,68);
209
+ var screen=new StackPanel();lcd.Child=screen;
210
+ screen.Children.Add(Text("DRUM BUDDY · PC MIXER",10,"#895329"));
211
+ lcdTop=Text("NEW SONG",19,"#704C2C");lcdTop.FontFamily=new FontFamily("Consolas");screen.Children.Add(lcdTop);
212
+ lcdBottom=Text("00:00:00 TR01",19,"#704C2C");lcdBottom.FontFamily=new FontFamily("Consolas");screen.Children.Add(lcdBottom);
213
+ Hot(980,316,29,30,"PAN / EQ",delegate { ShowMenu("Track settings"); });
214
+ Hot(1058,316,29,30,"SEND RETURN EFFECT",delegate { ShowMenu("Saved R16 effects"); });
215
+ Hot(1136,316,29,30,"INSERT EFFECT",delegate { ShowMenu("Saved R16 effects");Lcd("R16 FX FILE","Effect presets are preserved in native project copies. Zoom's internal DSP is not reproduced on the PC."); });
216
+ Hot(1214,316,29,30,"TOOL / settings",delegate { ShowMenu("USB & settings"); });
217
+ Hot(1292,316,29,30,"PROJECT / memory",delegate { ShowMenu("Projects & files"); });
218
+ Hot(979,377,31,30,"Bank tracks 1–8",delegate { SetBank(0); });Hot(1058,377,31,30,"Bank tracks 9–16",delegate { SetBank(8); });
219
+ Hot(1136,377,29,30,"USB / devices and SD card",delegate { ShowMenu("USB & settings"); });
220
+ Hot(1214,377,29,30,"BOUNCE / save WAV mix",async delegate { await ExportMix(); });
221
+ Hot(1292,377,29,30,"UNDO",delegate { Undo(); });
222
+ Hot(890,628,31,32,"Status button mode: mute, record arm, solo",delegate { statusMode=(statusMode+1)%3;Lcd("STATUS: "+StatusModeName(),"Track status buttons now toggle "+StatusModeName()+".");Sync(); });
223
+ Hot(880,353,56,66,"DIRECT / DAW BALANCE (hardware)",delegate { Hardware("DIRECT / DAW","Use the physical R16 DIRECT/DAW BALANCE knob to blend live inputs with PC playback."); });
224
+ Hot(194,306,27,24,"Hi-Z (hardware)",delegate { Hardware("INPUT 1 HI-Z","Hi-Z is a physical switch on the R16."); });
225
+ Hot(583,306,30,24,"Phantom power (hardware)",delegate { Hardware("PHANTOM POWER","Use the physical R16 phantom-power switch. This switch is not controllable over USB."); });
226
+ Hot(759,306,30,24,"Built-in microphones (hardware)",delegate { Hardware("BUILT-IN MIC","Select the built-in microphones using the physical R16 MIC switch."); });
227
+ Hot(892,306,29,24,"Metronome routing (hardware)",delegate { Hardware("METRONOME","The R16 metronome and its output routing are hardware functions."); });
228
+ Transport(984,684,60,38,"◀◀","Rewind 5 seconds",delegate { Seek(-5); });
229
+ Transport(1053,684,60,38,"▶▶","Forward 5 seconds",delegate { Seek(5); });
230
+ Transport(1122,684,60,38,"■","Stop & save",async delegate { if(recording) await StopTake();else { performance.StopPlayback();lock(mixer.Gate) mixer.Playing=false;mixer.Seek(0); } });
231
+ playButton=Transport(1190,684,60,38,"▶","Play / pause",delegate { PlayPause(); });
232
+ recordButton=Transport(1259,684,60,38,"●","Record armed inputs",async delegate { if(recording) await StopTake();else await StartTake(); },"#B52C36");
233
+ Hot(984,623,60,22,"F1 · toggle loop",delegate { ToggleLoop(); });Hot(1053,623,60,22,"F2 · set loop end",delegate { mixer.LoopEnd=mixer.Position;Lcd("LOOP END SET","Loop end set at "+Clock(mixer.LoopEnd)+"."); });
234
+ Hot(1122,623,60,22,"F3 · previous marker",delegate { Marker(false); });Hot(1190,623,60,22,"F4 · next marker",delegate { Marker(true); });Hot(1259,623,60,22,"F5 · add marker",delegate { markers.Add(mixer.Position);markers.Sort();Lcd("MARKER "+markers.Count,"Marker added at "+Clock(mixer.Position)+"."); });
235
+ Hot(1130,766,42,42,"EXIT / close menu",async delegate { if(sidebar.Visibility==Visibility.Visible) ToggleMenu();else await Leave(); });
236
+ Hot(1034,830,40,40,"ENTER / track settings",delegate { ShowMenu("Track settings"); });
237
+ Hot(992,826,32,45,"Previous track",delegate { Select(Math.Max(0,selected-1)); });Hot(1084,826,32,45,"Next track",delegate { Select(Math.Min(15,selected+1)); });
238
+ Hot(1032,791,45,30,"Previous dial parameter",delegate { parameter=(parameter+4)%5;Sync(); });Hot(1032,881,45,30,"Next dial parameter",delegate { parameter=(parameter+1)%5;Sync(); });
239
+ jog=new PanelDial { Minimum=-50,Maximum=50,SmallChange=1,Width=120,Height=120,ToolTip="Dial: pan, low EQ, mid EQ, high EQ or seek. Use the cursor up/down buttons to change its function." };
240
+ AutomationProperties.SetName(jog,"R16 data dial");At(jog,1188,790,120,120);
241
+ jog.ValueChanged+=delegate { if(updating || busy) return;Remember();lock(mixer.Gate) { TrackState t=mixer.Tracks[selected];if(parameter==0)t.Pan=(int)jog.Value;else if(parameter==1)t.LowGain=(int)jog.Value;else if(parameter==2)t.MidGain=(int)jog.Value;else if(parameter==3)t.HighGain=(int)jog.Value;else if(!recording)mixer.Seek(jog.Value); }dirty=true;Sync(false); };
242
+ }
243
+ private void At(FrameworkElement item,double x,double y,double width,double height) { item.Width=width;item.Height=height;Canvas.SetLeft(item,x-80);Canvas.SetTop(item,y-155);panel.Children.Add(item); }
244
+ private Button Hot(double x,double y,double width,double height,string label,Action action)
245
+ {
246
+ var button=new Button { Background=Brushes.Transparent,BorderThickness=new Thickness(0),ToolTip=label,Cursor=Cursors.Hand,Template=HotTemplate() };
247
+ AutomationProperties.SetName(button,label);button.Click+=delegate { action(); };At(button,x,y,width,height);return button;
248
+ }
249
+ private static ControlTemplate HotTemplate()
250
+ {
251
+ var border=new FrameworkElementFactory(typeof(Border));border.SetValue(Border.BackgroundProperty,Brushes.Transparent);border.SetValue(Border.CornerRadiusProperty,new CornerRadius(3));border.Name="Outline";
252
+ var template=new ControlTemplate(typeof(Button)) { VisualTree=border };
253
+ var hover=new Trigger { Property=UIElement.IsMouseOverProperty,Value=true };hover.Setters.Add(new Setter(Border.BorderBrushProperty,B("#007A6B"),"Outline"));hover.Setters.Add(new Setter(Border.BorderThicknessProperty,new Thickness(2),"Outline"));template.Triggers.Add(hover);
254
+ var focus=new Trigger { Property=UIElement.IsKeyboardFocusedProperty,Value=true };focus.Setters.Add(new Setter(Border.BorderBrushProperty,Brushes.DarkOrange,"Outline"));focus.Setters.Add(new Setter(Border.BorderThicknessProperty,new Thickness(2),"Outline"));template.Triggers.Add(focus);return template;
255
+ }
256
+ private Button Transport(double x,double y,double width,double height,string text,string label,Action action,string color=null)
257
+ {
258
+ var button=new Button { Content=text,FontSize=22,FontWeight=FontWeights.Bold,Foreground=color==null?B("#344941"):B(color),Background=B("#BFC6C2"),BorderBrush=B("#606D68"),ToolTip=label,Cursor=Cursors.Hand,Padding=new Thickness(0) };
259
+ AutomationProperties.SetName(button,label);button.Click+=delegate { action(); };At(button,x,y,width,height);return button;
260
+ }
261
+ private static TextBlock Text(string value,double size=15,string color="#E5EEEC") { return new TextBlock { Text=value,FontSize=size,Foreground=B(color) }; }
262
+ private static Button TextButton(string text,Action action) { var b=new Button { Content=text,Background=Brushes.Transparent,Foreground=B("#E7F0EE"),BorderThickness=new Thickness(0),Padding=new Thickness(6),Cursor=Cursors.Hand };b.Click+=delegate { action(); };return b; }
263
+ private Button MenuAction(string text,Action action) { var b=new Button { Content=text,Padding=new Thickness(10,7,10,7),MinHeight=40,FontSize=15,HorizontalContentAlignment=HorizontalAlignment.Left,Margin=new Thickness(0,3,0,3),Background=B("#E6ECEA"),Foreground=B("#1A302C"),BorderThickness=new Thickness(0),Cursor=Cursors.Hand };b.Click+=delegate { if(!busy && !finishing) action(); };return b; }
264
+ private readonly Dictionary<string,Expander> sections=new Dictionary<string,Expander>();
265
+ private void BuildSidebar(Grid root)
266
+ {
267
+ sidebar=new Border { Width=430,HorizontalAlignment=HorizontalAlignment.Left,VerticalAlignment=VerticalAlignment.Stretch,Margin=new Thickness(0,38,0,0),Background=B("#202D31"),BorderBrush=B("#698078"),BorderThickness=new Thickness(0,0,2,0),Padding=new Thickness(16),Visibility=Visibility.Collapsed };
268
+ Panel.SetZIndex(sidebar,20);root.Children.Add(sidebar);
269
+ var scroll=new ScrollViewer { VerticalScrollBarVisibility=ScrollBarVisibility.Auto,HorizontalScrollBarVisibility=ScrollBarVisibility.Disabled };sidebar.Child=scroll;
270
+ var stack=new StackPanel();scroll.Content=stack;
271
+ stack.Children.Add(Text("R16 · Files, settings & memory",20));
272
+ stack.Children.Add(TextButton("Close Menu",delegate { ToggleMenu(); }));
273
+ var projects=Section(stack,"Projects & files");
274
+ projectName=new TextBox { Text=song,MaxLength=8,FontSize=18,Margin=new Thickness(0,8,0,5) };
275
+ projectName.TextChanged+=delegate { if(!updating) { song=projectName.Text;dirty=true; } };projects.Children.Add(Label("Project name · up to 8 characters"));projects.Children.Add(projectName);
276
+ projects.Children.Add(MenuAction("Open R16 project / PRJDATA.ZDT…",async delegate { await ChooseNative(); }));
277
+ projects.Children.Add(MenuAction("Browse R16 SD card / memory folder…",delegate { BrowseMemory(); }));
278
+ projectList=new ComboBox { MinHeight=34,FontSize=15,Margin=new Thickness(0,4,0,4) };projects.Children.Add(projectList);
279
+ projects.Children.Add(MenuAction("Open selected memory project",async delegate { if(projectList.SelectedIndex>=0) await OpenNative(projectFolders[projectList.SelectedIndex]); }));
280
+ projectText=Note("In AUDIO INTERFACE mode the R16 card is unavailable. Use USB > CARD READER, or put the SD card in a PC card reader.");projects.Children.Add(projectText);
281
+ memoryFiles=new ListBox { Height=145,Background=B("#162024"),Foreground=B("#E7F0EE"),Margin=new Thickness(0,8,0,2),BorderBrush=B("#50645D") };projects.Children.Add(memoryFiles);
282
+ projects.Children.Add(MenuAction("Assign selected memory WAV to this track",async delegate { string path=memoryFiles.SelectedItem as string;if(path!=null && native!=null) await AddAudio(new[] { Path.Combine(native.Folder,"AUDIO",path) },selected); }));
283
+ projects.Children.Add(MenuAction("Add audio / video files…",async delegate { await ChooseAudio(); }));
284
+ projects.Children.Add(MenuAction("Save R16 project copy to SD card / folder…",async delegate { await SaveNative(); }));
285
+ projects.Children.Add(MenuAction("Save PC session… Ctrl+S",delegate { SaveSession(); }));
286
+ projects.Children.Add(MenuAction("Open PC session…",async delegate { await OpenSession(); }));
287
+ projects.Children.Add(MenuAction("Save stereo WAV mix…",async delegate { await ExportMix(); }));
288
+ var settings=Section(stack,"Track settings");
289
+ trackText=Text("Track 1",19);settings.Children.Add(trackText);
290
+ settings.Children.Add(Note("Faders, pan, mute, solo and EQ process PC playback. Recording keeps the raw R16 input. Solo is a PC audition setting."));
291
+ pan=Setting(settings,"Pan · L50 / C / R50",-50,50,delegate(int v) { mixer.Tracks[selected].Pan=v; });
292
+ var toggles=new WrapPanel();settings.Children.Add(toggles);
293
+ mute=Toggle(toggles,"Mute",delegate(bool v) { mixer.Tracks[selected].Mute=v; });solo=Toggle(toggles,"Solo",delegate(bool v) { mixer.Tracks[selected].Solo=v; });
294
+ arm=Toggle(toggles,"Record arm",delegate(bool v) { mixer.Tracks[selected].Arm=v; });invert=Toggle(toggles,"Invert phase",delegate(bool v) { mixer.Tracks[selected].Invert=v; });
295
+ stereo=Toggle(toggles,"Stereo link",delegate(bool v) { mixer.Tracks[selected].Stereo=v; });
296
+ settings.Children.Add(Note("Record input = track number within its bank: tracks 1/9 use input 1, 2/10 input 2, etc. Arm up to 8 tracks. Stereo link is a saved R16 setting; each PC strip can play mono or stereo audio."));
297
+ low=Setting(settings,"LOW EQ gain · dB",-12,12,delegate(int v) { mixer.Tracks[selected].LowGain=v; });
298
+ lowFrequency=FrequencyBox(settings,"Low frequency",R16Project.Frequencies.Take(17).ToArray(),delegate(int v) { mixer.Tracks[selected].LowFreq=v; });lowOn=Toggle(settings,"Low EQ enabled",delegate(bool v) { mixer.Tracks[selected].LowOn=v; });
299
+ mid=Setting(settings,"MID EQ gain · dB",-12,12,delegate(int v) { mixer.Tracks[selected].MidGain=v; });
300
+ midFrequency=FrequencyBox(settings,"Mid frequency",R16Project.Frequencies,delegate(int v) { mixer.Tracks[selected].MidFreq=v; });
301
+ midQ=new ComboBox { ItemsSource=Enumerable.Range(1,10).Select(n=>n/10.0).ToArray(),MinHeight=32,Margin=new Thickness(0,4,0,4) };settings.Children.Add(Label("Mid EQ Q"));settings.Children.Add(midQ);
302
+ midQ.SelectionChanged+=delegate { if(!updating && midQ.SelectedItem!=null) Edit(delegate { mixer.Tracks[selected].MidQ=(double)midQ.SelectedItem; }); };
303
+ midOn=Toggle(settings,"Mid EQ enabled",delegate(bool v) { mixer.Tracks[selected].MidOn=v; });
304
+ high=Setting(settings,"HIGH EQ gain · dB",-12,12,delegate(int v) { mixer.Tracks[selected].HighGain=v; });
305
+ highFrequency=FrequencyBox(settings,"High frequency",R16Project.Frequencies.Skip(11).ToArray(),delegate(int v) { mixer.Tracks[selected].HighFreq=v; });highOn=Toggle(settings,"High EQ enabled",delegate(bool v) { mixer.Tracks[selected].HighOn=v; });
306
+ settings.Children.Add(MenuAction("Remove audio from this PC track",delegate { if(recording)return;Remember();mixer.Attach(selected,null);dirty=true;Sync(); }));
307
+ var fx=Section(stack,"Saved R16 effects");
308
+ fxText=Note("Open an R16 project to view its saved effect-preset names.");fx.Children.Add(fxText);
309
+ fx.Children.Add(Note("These controls edit the R16 project copy. The R16's proprietary reverb, chorus and insert DSP is preserved in EFXDATA.ZDT and runs on the R16. PC preview uses volume, pan and EQ only."));
310
+ reverb=Setting(fx,"Selected track · reverb send (0–100)",0,100,delegate(int v) { mixer.Tracks[selected].Reverb=v; });reverbOn=Toggle(fx,"Reverb send enabled in R16 project",delegate(bool v) { mixer.Tracks[selected].ReverbOn=v; });
311
+ chorus=Setting(fx,"Selected track · chorus send (0–100)",0,100,delegate(int v) { mixer.Tracks[selected].Chorus=v; });chorusOn=Toggle(fx,"Chorus send enabled in R16 project",delegate(bool v) { mixer.Tracks[selected].ChorusOn=v; });
312
+ var notes=Section(stack,"MIDI instrument");
313
+ notes.Children.Add(Note("Connect the keyboard or drum kit directly to this PC by USB MIDI. Its audio outputs can connect to the R16. Instrument notes are separate from R16 fader/transport MIDI."));
314
+ notes.Children.Add(Label("Instrument MIDI input"));noteInput=new ComboBox { MinHeight=34,Margin=new Thickness(0,4,0,6) };notes.Children.Add(noteInput);
315
+ notes.Children.Add(Label("MIDI playback output"));noteOutput=new ComboBox { MinHeight=34,Margin=new Thickness(0,4,0,6) };notes.Children.Add(noteOutput);
316
+ notes.Children.Add(MenuAction("Connect instrument / MIDI output",delegate {
317
+ if(recording)return;
318
+ try { var input=noteInput.SelectedItem as MidiPort;var output=noteOutput.SelectedItem as MidiPort;if((input!=null && IsR16(input.Name)) || (output!=null && IsR16(output.Name)))throw new IOException("Choose the instrument's port here. The R16 port is reserved for its control surface.");performance.Connect(input,output);Lcd("INSTRUMENT MIDI",performance.Connected?"Instrument connected. Play a note to check the monitor below.":"MIDI output connected. Open a .MID file to play it."); }catch(Exception ex) { Lcd("MIDI INSTRUMENT",ex.Message); }
319
+ }));
320
+ noteStatus=Note("No MIDI notes received");notes.Children.Add(noteStatus);
321
+ noteArm=new CheckBox { Content="Arm MIDI note recording",Foreground=B("#E4EEEB"),FontSize=15,Margin=new Thickness(0,8,0,8) };notes.Children.Add(noteArm);
322
+ notes.Children.Add(MenuAction("Record MIDI + any armed audio inputs",async delegate { noteArm.IsChecked=true;await StartTake(); }));
323
+ notes.Children.Add(MenuAction("Stop & save recording",async delegate { if(recording)await StopTake();else performance.StopPlayback(); }));
324
+ notes.Children.Add(MenuAction("Open .MID file…",delegate { if(recording)return;var d=new Microsoft.Win32.OpenFileDialog { Title="Open MIDI performance",Filter="Standard MIDI file|*.mid;*.midi" };if(d.ShowDialog(window)==true) { try { performance.Load(d.FileName);Lcd("MIDI FILE LOADED",d.FileName); }catch(Exception ex) { Lcd("MIDI FILE",ex.Message); } } }));
325
+ notes.Children.Add(MenuAction("Play MIDI file to selected output",async delegate { try { await performance.Play(); }catch(Exception ex) { Lcd("MIDI PLAYBACK",ex.Message); } }));
326
+ notes.Children.Add(MenuAction("Stop MIDI playback",delegate { performance.StopPlayback(); }));
327
+ notes.Children.Add(Note("MIDI stores notes, timing, velocity and controllers rather than sound. Choose your instrument's output, or Microsoft GS Wavetable Synth if Windows provides it. MIDI playback is separate from the WAV mix."));
328
+ var devices=Section(stack,"USB & settings");
329
+ devices.Children.Add(Label("Audio device · 44.1 kHz"));outputDevice=new ComboBox { MinHeight=36,FontSize=14,Margin=new Thickness(0,4,0,5) };devices.Children.Add(outputDevice);
330
+ outputDevice.SelectionChanged+=delegate { if(!updating && !recording) { lock(mixer.Gate)mixer.Playing=false;audio.Dispose(); } };
331
+ devices.Children.Add(MenuAction("Connect audio / live input meters",delegate { EnsureAudio(); }));
332
+ devices.Children.Add(Label("Main meter source"));
333
+ meterSource=new ComboBox { ItemsSource=new[] { "Auto · inputs while idle/recording, tracks during playback", "R16 physical inputs 1–8", "PC playback tracks in the selected bank" },SelectedIndex=0,MinHeight=36,FontSize=13,Margin=new Thickness(0,4,0,7) };
334
+ devices.Children.Add(meterSource);
335
+ devices.Children.Add(MenuAction("Disconnect audio to use CARD READER",delegate { if(recording) { Lcd("STOP FIRST","Stop and save the take before changing USB mode.");return; }lock(mixer.Gate)mixer.Playing=false;audio.Dispose();midi.Dispose();Lcd("CARD READER MODE","On the R16 terminate AUDIO INTERFACE, then choose USB > CARD READER. Browse the mounted card in Projects & files."); }));
336
+ devices.Children.Add(Label("MIDI input"));midiInput=new ComboBox { MinHeight=34,Margin=new Thickness(0,4,0,6) };devices.Children.Add(midiInput);
337
+ devices.Children.Add(Label("MIDI output"));midiOutput=new ComboBox { MinHeight=34,Margin=new Thickness(0,4,0,6) };devices.Children.Add(midiOutput);
338
+ devices.Children.Add(MenuAction("Connect R16 faders & transport",delegate { TryMidi(); }));
339
+ devices.Children.Add(MenuAction("Refresh connected devices",delegate { RefreshDevices(); }));
340
+ devices.Children.Add(Label("WAV quality"));var bitPanel=new StackPanel { Orientation=Orientation.Horizontal,Margin=new Thickness(0,6,0,10) };devices.Children.Add(bitPanel);
341
+ bits16=new RadioButton { Content="16-bit",GroupName="ControlBits",Margin=new Thickness(0,0,20,0) };bits24=new RadioButton { Content="24-bit",GroupName="ControlBits",IsChecked=true };bitPanel.Children.Add(bits16);bitPanel.Children.Add(bits24);
342
+ bits16.Checked+=delegate { if(!updating){bits=16;dirty=true;} };bits24.Checked+=delegate { if(!updating){bits=24;dirty=true;} };
343
+ devices.Children.Add(Note("Use the physical R16 gain, Hi-Z, phantom power, MIC and DIRECT/DAW controls. They do not expose remote values. MIDI controls the PC mixer; its connection does not synchronize the R16's standalone SD-recorder state."));
344
+ devices.Children.Add(TextButton("Cancel file operation",delegate { if(cancellation!=null)cancellation.Cancel(); }));
345
+ devices.Children.Add(Note("R16 front-panel image © Zoom Corporation. Drum Buddy is an independent utility, not an official Zoom product."));
346
+ }
347
+ private StackPanel Section(StackPanel parent,string title) { var content=new StackPanel { Margin=new Thickness(0,10,7,16) };var section=new Expander { Header=title,Content=content,Foreground=B("#E4EEEB"),FontSize=18,Margin=new Thickness(0,8,0,4),IsExpanded=false };parent.Children.Add(section);sections[title]=section;return content; }
348
+ private TextBlock Note(string text) { var note=Text(text,13,"#B7CCC5");note.TextWrapping=TextWrapping.Wrap;note.Margin=new Thickness(0,5,0,8);return note; }
349
+ private TextBlock Label(string text) { var label=Text(text,14);label.Margin=new Thickness(0,9,0,3);return label; }
350
+ private Slider Setting(StackPanel parent,string label,int min,int max,Action<int> setter)
351
+ {
352
+ var text=Label(label);parent.Children.Add(text);
353
+ var slider=new Slider { Minimum=min,Maximum=max,TickFrequency=1,IsSnapToTickEnabled=true,SmallChange=1,Height=28,Margin=new Thickness(0,3,0,4),ToolTip=label };
354
+ AutomationProperties.SetName(slider,label);parent.Children.Add(slider);slider.ValueChanged+=delegate { text.Text=label+" "+((int)slider.Value).ToString(inv);if(!updating) Edit(delegate { setter((int)slider.Value); }); };return slider;
355
+ }
356
+ private CheckBox Toggle(Panel parent,string text,Action<bool> setter) { var check=new CheckBox { Content=text,Foreground=B("#E4EEEB"),FontSize=14,Margin=new Thickness(0,5,16,5) };parent.Children.Add(check);check.Checked+=delegate { if(!updating)Edit(delegate { setter(true); }); };check.Unchecked+=delegate { if(!updating)Edit(delegate { setter(false); }); };return check; }
357
+ private ComboBox FrequencyBox(StackPanel parent,string title,int[] values,Action<int> setter) { parent.Children.Add(Label(title));var combo=new ComboBox { ItemsSource=values,MinHeight=32,FontSize=14 };parent.Children.Add(combo);combo.SelectionChanged+=delegate { if(!updating && combo.SelectedItem!=null)Edit(delegate { setter((int)combo.SelectedItem); }); };return combo; }
358
+ private void Edit(Action action) { if(busy || finishing)return;Remember();lock(mixer.Gate)action();dirty=true;Sync(false); }
359
+ private void Remember() { if(updating || recording)return;if(undo.Count<100)undo.Push(mixer.Snapshot(song,bits,native==null?"":native.Folder)); }
360
+ private void Undo() { if(recording || busy || undo.Count==0)return;MixerSession s=undo.Pop();for(int i=0;i<16;i++) { s.Tracks[i].File=mixer.Tracks[i].File;s.Tracks[i].Name=mixer.Tracks[i].Name;s.Tracks[i].Offset=mixer.Tracks[i].Offset; }mixer.Apply(s);song=s.Name;bits=s.Bits;dirty=true;Sync();Lcd("MIXER UNDO","Restored the previous mixer settings."); }
361
+ private string StatusModeName() { return statusMode==0?"MUTE":statusMode==1?"RECORD ARM":"SOLO"; }
362
+ private void ToggleStatus(int index) { if(busy || (recording && statusMode==1))return;selected=index;Edit(delegate { var t=mixer.Tracks[index];if(statusMode==0)t.Mute=!t.Mute;else if(statusMode==1)t.Arm=!t.Arm;else t.Solo=!t.Solo; }); }
363
+ private void Select(int index) { selected=index;bank=index<8?0:8;Sync(); }
364
+ private void SetBank(int value) { bank=value;selected=bank+(selected%8);Sync();Lcd("BANK "+(bank+1)+"–"+(bank+8),"Hardware faders now control PC tracks "+(bank+1)+"–"+(bank+8)+"."); }
365
+ private void ToggleMenu() { bool show=sidebar.Visibility!=Visibility.Visible;sidebar.Visibility=show?Visibility.Visible:Visibility.Collapsed;menuButton.Content=show?"Close Menu":"Open Menu"; }
366
+ private void ShowMenu(string section) { sidebar.Visibility=Visibility.Visible;menuButton.Content="Close Menu";foreach(var pair in sections)pair.Value.IsExpanded=pair.Key==section;sections[section].BringIntoView();Sync(); }
367
+ private void Hardware(string title,string message) { Lcd(title,message); }
368
+ private void Lcd(string title,string message) { temporaryLcd=title;lcdUntil=DateTime.Now.AddSeconds(4);if(notice!=null)notice.Text=message;UpdateLcd(); }
369
+ private static string Clock(double seconds) { var t=TimeSpan.FromSeconds(Math.Max(0,seconds));return ((int)t.TotalHours).ToString("00")+":"+t.Minutes.ToString("00")+":"+t.Seconds.ToString("00"); }
370
+ private void UpdateLcd()
371
+ {
372
+ if(lcdTop==null)return;
373
+ string[] parameters={"PAN","LOW","MID","HIGH","SEEK"};
374
+ lcdTop.Text=DateTime.Now<lcdUntil?temporaryLcd:(recording?"REC ":mixer.Playing?"PLAY ":"STOP ")+song;
375
+ lcdBottom.Text=Clock(recording && recordedTracks.Length==0?recordStart+performance.RecordSeconds:mixer.Position)+" T"+(selected+1).ToString("00")+" "+parameters[parameter];
376
+ }
377
+ private void Sync(bool sliders=true)
378
+ {
379
+ if(window==null)return;updating=true;
380
+ try {
381
+ TrackState t=mixer.Tracks[selected];
382
+ for(int i=0;i<8;i++) {
383
+ TrackState state=mixer.Tracks[bank+i];
384
+ if(sliders)faders[i].Value=state.Fader;
385
+ statusLeds[i].State=PanelFeedback.TrackLamp(state);
386
+ AutomationProperties.SetName(faders[i],"Track "+(bank+i+1)+" fader");
387
+ AutomationProperties.SetName(statusButtons[i],"Track "+(bank+i+1)+" · "+StatusModeName()+" · "+(state.Arm?"Armed":state.Mute?"Muted":state.Solo?"Solo":"Play"));
388
+ }
389
+ bankLeds[0].State=bank==0?LampState.Red:LampState.Off;bankLeds[1].State=bank==8?LampState.Red:LampState.Off;
390
+ modeLamp.State=statusMode==1?LampState.Red:statusMode==2?LampState.Amber:LampState.Green;
391
+ if(sliders) faders[8].Value=mixer.Master;
392
+ for(int i=0;i<16;i++) { var tr=mixer.Tracks[i];overviewText[i].Text=(i+1)+(tr.Arm?" ●":tr.Solo?" S":tr.Mute?" M":"")+" · "+(tr.Name==""?"Empty":tr.Name);overview[i].BorderBrush=i==selected?B("#72DFC1"):B("#4B5A60");overview[i].BorderThickness=new Thickness(i==selected?2:1);overview[i].ToolTip="Track "+(i+1)+" · "+tr.Name+" · "+MixerEngine.FaderDb(tr.Fader).ToString("0.0",inv)+" dB"; }
393
+ trackText.Text="Track "+(selected+1)+" · "+(t.Name==""?"Empty":t.Name);
394
+ pan.Value=t.Pan;low.Value=t.LowGain;mid.Value=t.MidGain;high.Value=t.HighGain;reverb.Value=t.Reverb;chorus.Value=t.Chorus;
395
+ mute.IsChecked=t.Mute;solo.IsChecked=t.Solo;arm.IsChecked=t.Arm;invert.IsChecked=t.Invert;stereo.IsChecked=t.Stereo;
396
+ lowOn.IsChecked=t.LowOn;midOn.IsChecked=t.MidOn;highOn.IsChecked=t.HighOn;reverbOn.IsChecked=t.ReverbOn;chorusOn.IsChecked=t.ChorusOn;
397
+ lowFrequency.SelectedItem=t.LowFreq;midFrequency.SelectedItem=t.MidFreq;highFrequency.SelectedItem=t.HighFreq;midQ.SelectedItem=t.MidQ;
398
+ projectName.Text=song;bits16.IsChecked=bits==16;bits24.IsChecked=bits!=16;
399
+ jog.Minimum=parameter==0?-50:parameter==4?0:-12;jog.Maximum=parameter==0?50:parameter==4?Math.Max(1,mixer.Duration):12;
400
+ jog.Value=parameter==0?t.Pan:parameter==1?t.LowGain:parameter==2?t.MidGain:parameter==3?t.HighGain:mixer.Position;
401
+ UpdateHeader();
402
+ fxText.Text=native==null?"Open an R16 project to view saved effect presets.":"Reverb: "+native.ReverbPatch+"\nChorus / delay: "+native.ChorusPatch+"\nInsert effect: stored in EFXDATA.ZDT; copied unchanged.";
403
+ if(midi.Connected) { for(int i=0;i<8;i++){var tr=mixer.Tracks[bank+i];midi.Led(i,tr.Arm);midi.Led(8+i,tr.Solo);midi.Led(16+i,tr.Mute);midi.Led(24+i,bank+i==selected);}midi.Led(94,mixer.Playing);midi.Led(95,recording); }
404
+ } finally { updating=false; }
405
+ UpdateLcd();
406
+ UpdateTransportLights();
407
+ }
408
+ private void UpdateHeader() { deviceText.Text="R16 Full Control · Bank "+(bank+1)+"–"+(bank+8)+" · "+(audio.Connected?(audio.CanRecord?"R16 AUDIO ON":"PC AUDIO ON"):"AUDIO OFF")+" · "+(midi.Connected?(Interlocked.Read(ref midi.Messages)>0?"MIDI receiving":"MIDI awaiting movement"):"MIDI not connected")+(recording?" · RECORDING":""); }
409
+ private void UpdateTransportLights()
410
+ {
411
+ bool pulse=(DateTime.UtcNow.Ticks/TimeSpan.TicksPerMillisecond/450)%2==0;
412
+ bool armed=mixer.Tracks.Any(t=>t.Arm) || (noteArm.IsChecked==true && performance.Connected);
413
+ bool red=PanelFeedback.RecordLamp(recording,armed,pulse);
414
+ recordButton.Background=red?B("#FF4233"):B("#BFC6C2");recordButton.Foreground=red?Brushes.White:B("#882C29");
415
+ recordButton.BorderThickness=new Thickness(red?2:1);recordButton.BorderBrush=red?B("#FF927B"):B("#606D68");
416
+ playButton.Background=mixer.Playing?B("#46D68D"):B("#BFC6C2");playButton.Foreground=mixer.Playing?B("#063B24"):B("#344941");
417
+ AutomationProperties.SetItemStatus(recordButton,recording?"Recording":armed?"Armed":"Not armed");
418
+ }
419
+ private void RefreshDevices()
420
+ {
421
+ if(recording || busy)return;
422
+ updating=true;
423
+ try {
424
+ var choices=AudioOutputs.Choices();
425
+ string previous=outputDevice.SelectedItem==null?null:outputDevice.SelectedItem.ToString();
426
+ outputDevice.ItemsSource=choices;outputDevice.SelectedItem=choices.FirstOrDefault(c=>c.Name==previous)??choices.FirstOrDefault(c=>c.Driver!=null && IsR16(c.Name))??choices[0];
427
+ string input=midiInput.SelectedItem==null?null:midiInput.SelectedItem.ToString(),output=midiOutput.SelectedItem==null?null:midiOutput.SelectedItem.ToString();
428
+ var ins=R16Midi.Inputs();var outs=R16Midi.Outputs();midiInput.ItemsSource=ins;midiOutput.ItemsSource=outs;
429
+ midiInput.SelectedItem=ins.FirstOrDefault(p=>p.Name==input)??ins.FirstOrDefault(p=>IsR16(p.Name));midiOutput.SelectedItem=outs.FirstOrDefault(p=>p.Name==output)??outs.FirstOrDefault(p=>IsR16(p.Name));
430
+ string instrument=noteInput.SelectedItem==null?null:noteInput.SelectedItem.ToString(),instrumentOut=noteOutput.SelectedItem==null?null:noteOutput.SelectedItem.ToString();
431
+ noteInput.ItemsSource=ins.Where(p=>!IsR16(p.Name)).ToArray();noteOutput.ItemsSource=outs.Where(p=>!IsR16(p.Name)).ToArray();
432
+ noteInput.SelectedItem=ins.FirstOrDefault(p=>p.Name==instrument);noteOutput.SelectedItem=outs.FirstOrDefault(p=>p.Name==instrumentOut)??outs.FirstOrDefault(p=>p.Name.IndexOf("GS Wavetable",StringComparison.OrdinalIgnoreCase)>=0);
433
+ } catch(Exception ex) { Lcd("DEVICES",ex.Message); }finally { updating=false; }Sync();
434
+ }
435
+ private static bool IsR16(string text) { return text.IndexOf("R16",StringComparison.OrdinalIgnoreCase)>=0 || text.IndexOf("R24",StringComparison.OrdinalIgnoreCase)>=0; }
436
+ private void TryMidi() { if(recording)return;try { midi.Connect(midiInput.SelectedItem as MidiPort,midiOutput.SelectedItem as MidiPort);Lcd("MIDI",midi.Status); }catch(Exception ex) { Lcd("MIDI",ex.Message); }Sync(); }
437
+ private bool EnsureAudio()
438
+ {
439
+ if(audio.Connected)return true;
440
+ try { audio.Connect(mixer,outputDevice.SelectedItem as OutputChoice);Lcd("AUDIO CONNECTED",audio.CanRecord?"R16 audio connected. Set DIRECT/DAW BALANCE toward DAW to hear PC playback; use direct monitoring for live inputs.":"PC playback connected. Select the Zoom ASIO device in Open Menu to record R16 inputs.");return true; }
441
+ catch(Exception ex) { Lcd("AUDIO",ex.Message+" Check AUDIO INTERFACE mode and close other audio apps.");return false; }
442
+ }
443
+ private void PlayPause() { if(busy || finishing || recording)return;if(!EnsureAudio())return;lock(mixer.Gate) { if(!mixer.Playing && mixer.Position>=mixer.Duration)mixer.Seek(0);mixer.Playing=!mixer.Playing; }Sync(); }
444
+ private void Seek(double delta) { if(!recording && !busy)mixer.Seek(Math.Max(0,Math.Min(mixer.Duration,mixer.Position+delta)));UpdateLcd(); }
445
+ private void ToggleLoop() { if(recording)return;lock(mixer.Gate) { if(mixer.LoopEnd<=mixer.LoopStart+.01) { mixer.LoopStart=0;mixer.LoopEnd=mixer.Duration; }mixer.Loop=!mixer.Loop; }Lcd(mixer.Loop?"LOOP ON":"LOOP OFF","Loop "+Clock(mixer.LoopStart)+" to "+Clock(mixer.LoopEnd)+". F2 sets the end at the current position."); }
446
+ private void Marker(bool forward) { if(recording)return;double p=mixer.Position;double next=forward?markers.FirstOrDefault(m=>m>p+.1):markers.Where(m=>m<p-.1).DefaultIfEmpty(0).Last();if(forward && next==0)next=mixer.Duration;mixer.Seek(next); }
447
+ private async void Midi(McuAction a)
448
+ {
449
+ if(finishing)return;int index=bank+a.Channel;
450
+ switch(a.Kind) {
451
+ case "fader":Edit(delegate { if(a.Channel==8)mixer.Master=a.Value;else { selected=index;mixer.Tracks[index].Fader=a.Value; } });Sync();break;
452
+ case "pan":Edit(delegate { selected=index;mixer.Tracks[index].Pan=Math.Max(-50,Math.Min(50,mixer.Tracks[index].Pan+a.Value)); });break;
453
+ case "mute":Edit(delegate { selected=index;mixer.Tracks[index].Mute=!mixer.Tracks[index].Mute; });break;
454
+ case "solo":Edit(delegate { selected=index;mixer.Tracks[index].Solo=!mixer.Tracks[index].Solo; });break;
455
+ case "arm":if(!recording)Edit(delegate { selected=index;mixer.Tracks[index].Arm=!mixer.Tracks[index].Arm; });break;
456
+ case "select":Select(index);break;case "bank":SetBank(a.Value);break;case "play":PlayPause();break;
457
+ case "stop":if(recording)await StopTake();else { performance.StopPlayback();lock(mixer.Gate)mixer.Playing=false; }break;
458
+ case "record":if(recording)await StopTake();else await StartTake();break;
459
+ case "seek":case "jog":Seek(a.Value*(a.Kind=="jog"?.1:1));break;
460
+ case "track":Select(Math.Max(0,Math.Min(15,selected+a.Value)));break;
461
+ case "parameter":parameter=(parameter+a.Value+5)%5;Sync();break;
462
+ case "loop":ToggleLoop();break;case "loopend":mixer.LoopEnd=mixer.Position;break;
463
+ case "previous":Marker(false);break;case "next":Marker(true);break;case "mark":markers.Add(mixer.Position);markers.Sort();break;
464
+ case "undo":Undo();break;case "save":SaveSession();break;
465
+ }
466
+ }
467
+ private async Task Tick()
468
+ {
469
+ if(window==null || !window.IsVisible || disposed)return;
470
+ bool playing=mixer.Playing;
471
+ float[] playback=new float[16];float left,right;
472
+ lock(mixer.Gate) {
473
+ for(int i=0;i<16;i++) { playback[i]=mixer.Peaks[i];mixer.Peaks[i]=0; }
474
+ left=mixer.MasterLeftPeak;right=mixer.MasterRightPeak;mixer.MasterPeak=0;mixer.MasterLeftPeak=0;mixer.MasterRightPeak=0;
475
+ }
476
+ float[] inputs=audio.DrainInputPeaks();
477
+ bool inputMode=meterSource.SelectedIndex==1 || (meterSource.SelectedIndex==0 && (recording || !playing));
478
+ for(int i=0;i<16;i++) { trackLevels[i].Update(playback[i],.075);overviewMeters[i].Value=Math.Max(0,Math.Min(1,(trackLevels[i].Db+60)/60)); }
479
+ for(int i=0;i<8;i++) {
480
+ mainMeters[i].Update(inputMode?inputs[i]:playback[bank+i],.075,inputMode?"IN "+(i+1):"TR "+(bank+i+1));
481
+ if(inputs[i]>=.99f)inputClipHold[i]=1.5;else inputClipHold[i]=Math.Max(0,inputClipHold[i]-.075);
482
+ inputClipLeds[i].State=inputClipHold[i]>0?LampState.Red:LampState.Off;
483
+ }
484
+ mainMeters[8].Update(left,.075,"L");mainMeters[9].Update(right,.075,"R");
485
+ if(lastPlaying!=playing || lastRecording!=recording) { lastPlaying=playing;lastRecording=recording;Sync(false); }
486
+ UpdateTransportLights();UpdateHeader();
487
+ UpdateLcd();
488
+ noteStatus.Text=performance.LastEvent+"\nNotes received: "+Interlocked.Read(ref performance.NoteCount)+(performance.LoadedPath==null?"":"\nFile: "+Path.GetFileName(performance.LoadedPath));
489
+ if(dirty && !busy && !recording && DateTime.Now-lastRecovery>TimeSpan.FromSeconds(2))SaveRecovery();
490
+ if(audio.Error!=null || mixer.Error!=null || audio.TakeError!=null || performance.Error!=null) {
491
+ Exception error=audio.Error??mixer.Error??audio.TakeError??performance.Error;
492
+ if(recording && !finishing)await StopTake();
493
+ audio.Dispose();audio.Error=null;mixer.Error=null;performance.Error=null;lock(mixer.Gate)mixer.Playing=false;
494
+ Lcd("AUDIO STOPPED",error.Message+" Any captured audio was kept.");
495
+ }
496
+ }
497
+ private void Work(bool value,string message) { busy=value;notice.Text=message;foreach(var f in faders)f.IsEnabled=!value;projectName.IsEnabled=!value && !recording;outputDevice.IsEnabled=!value && !recording;bits16.IsEnabled=!value && !recording;bits24.IsEnabled=!value && !recording;if(!value && leaveAfterWork) { leaveAfterWork=false;window.Dispatcher.BeginInvoke((Action)(async delegate { await Leave(); })); } }
498
+ private string FolderDialog(string description,string initial) { using(var dialog=new System.Windows.Forms.FolderBrowserDialog { Description=description,SelectedPath=Directory.Exists(initial)?initial:"",ShowNewFolderButton=true })return dialog.ShowDialog()==System.Windows.Forms.DialogResult.OK?dialog.SelectedPath:null; }
499
+ private async Task ChooseAudio() { var d=new Microsoft.Win32.OpenFileDialog { Title="Add tracks · MP3, MP4, WAV and more",Multiselect=true,Filter="Audio / video|*.wav;*.mp3;*.mp4;*.m4a;*.flac;*.ogg;*.aif;*.aiff;*.mov;*.mkv;*.wma;*.aac|All files|*.*" };if(d.ShowDialog(window)==true)await AddAudio(d.FileNames,selected); }
500
+ private async Task AddAudio(string[] paths,int start)
501
+ {
502
+ if(busy || recording)return;
503
+ string midiPath=paths.FirstOrDefault(p=>string.Equals(Path.GetExtension(p),".mid",StringComparison.OrdinalIgnoreCase) || string.Equals(Path.GetExtension(p),".midi",StringComparison.OrdinalIgnoreCase));
504
+ if(midiPath!=null) { try { performance.Load(midiPath);Lcd("MIDI FILE LOADED","Open Menu > MIDI instrument to choose an output and play this file."); }catch(Exception ex) { Lcd("MIDI FILE",ex.Message); } }
505
+ paths=paths.Where(p=>!string.Equals(Path.GetExtension(p),".mid",StringComparison.OrdinalIgnoreCase) && !string.Equals(Path.GetExtension(p),".midi",StringComparison.OrdinalIgnoreCase)).ToArray();if(paths.Length==0)return;
506
+ lock(mixer.Gate)mixer.Playing=false;Work(true,"Reading audio tracks…");cancellation=new CancellationTokenSource();int added=0;
507
+ try {
508
+ foreach(string path in paths.Where(File.Exists)) {
509
+ int index=start+added;if(index>=16)break;
510
+ Clip clip=await Task.Run(()=>Audio.Load(path,cache,cancellation.Token,null));mixer.Attach(index,clip);lock(mixer.Gate) { mixer.Tracks[index].Offset=0;mixer.Tracks[index].Mute=false; }added++;
511
+ }
512
+ dirty=true;Lcd("TRACKS LOADED",added+" track(s) loaded. Press Play to hear the PC mix.");
513
+ }catch(OperationCanceledException) { notice.Text="Import cancelled. Loaded tracks were kept."; }catch(Exception ex) { notice.Text="Could not read audio: "+ex.Message; }
514
+ finally { cancellation.Dispose();cancellation=null;Work(false,notice.Text);Sync(); }
515
+ }
516
+ private async Task ChooseNative() { if(recording)return;var d=new Microsoft.Win32.OpenFileDialog { Title="Open an R16 project",Filter="R16 project|PRJDATA.ZDT|All files|*.*" };if(d.ShowDialog(window)==true)await OpenNative(Path.GetDirectoryName(d.FileName)); }
517
+ private void BrowseMemory()
518
+ {
519
+ if(recording)return;string folder=FolderDialog("Choose the R16 SD card or a folder containing PROJ000, PROJ001…",native==null?library:native.Folder);if(folder==null)return;
520
+ try {
521
+ projectFolders.Clear();var candidates=File.Exists(Path.Combine(folder,"PRJDATA.ZDT"))?new[] {folder}:Directory.GetDirectories(folder,"PROJ???");
522
+ var names=new List<string>();foreach(string candidate in candidates.OrderBy(p=>p)) { if(!File.Exists(Path.Combine(candidate,"PRJDATA.ZDT")))continue;try { var project=R16Project.Read(candidate);projectFolders.Add(candidate);names.Add(Path.GetFileName(candidate)+" · "+project.Name+" · "+project.Bits+"-bit"); }catch(IOException) { } }
523
+ projectList.ItemsSource=names;if(names.Count>0)projectList.SelectedIndex=0;projectText.Text=names.Count+" R16 project(s) found in "+folder;
524
+ }catch(Exception ex) { Lcd("MEMORY",ex.Message); }
525
+ }
526
+ private async Task OpenNative(string folder)
527
+ {
528
+ if(busy || recording)return;lock(mixer.Gate)mixer.Playing=false;audio.Dispose();Work(true,"Copying R16 project to the PC library…");cancellation=new CancellationTokenSource();
529
+ try {
530
+ R16Project source=await Task.Run(()=>R16Project.Read(folder));
531
+ string incoming=Path.Combine(library,"Imported_"+DateTime.Now.ToString("yyyyMMdd_HHmmss_fff",inv));
532
+ string copy=await Task.Run(()=>R16Project.CopyFolder(source.Folder,incoming,cancellation.Token));
533
+ var project=R16Project.Read(copy);
534
+ for(int i=0;i<16;i++)mixer.Attach(i,null);
535
+ var session=new MixerSession { Name=project.Name,Bits=project.Bits,Master=project.Master,Tracks=project.Tracks.Select(t=>t.Copy()).ToArray() };mixer.Apply(session);
536
+ native=project;song=project.Name;bits=project.Bits;sessionPath=null;mixer.Seek(0);undo.Clear();int missing=0;
537
+ for(int i=0;i<16;i++) {
538
+ string path=project.Tracks[i].File;if(string.IsNullOrEmpty(path))continue;
539
+ if(!File.Exists(path)) { missing++;continue; }
540
+ Clip clip=await Task.Run(()=>Audio.Load(path,cache,cancellation.Token,null));mixer.Attach(i,clip);
541
+ }
542
+ dirty=false;memoryFiles.ItemsSource=Directory.Exists(Path.Combine(copy,"AUDIO"))?Directory.GetFiles(Path.Combine(copy,"AUDIO"),"*.WAV").Select(Path.GetFileName).OrderBy(p=>p).ToArray():new string[0];
543
+ projectText.Text="PC working copy: "+copy+"\nOriginal: "+folder+"\n"+(project.Protected?"Original project protection is preserved.":"16 track assignments and saved mixer settings loaded.");
544
+ Lcd("PROJECT LOADED",missing==0?"R16 project copied to the PC. The original card was kept unchanged.":missing+" assigned WAV(s) are missing. Available audio and metadata were kept.");
545
+ }catch(OperationCanceledException) { Lcd("CANCELLED","Project loading cancelled. Any copied files remain in the PC library."); }
546
+ catch(Exception ex) { Lcd("PROJECT",ex.Message); }
547
+ finally { cancellation.Dispose();cancellation=null;Work(false,notice.Text);Sync(); }
548
+ }
549
+ private async Task SaveNative()
550
+ {
551
+ if(busy || recording)return;if(native==null) { Lcd("OPEN A PROJECT","Open an existing R16 project first. Its native settings and effects files are needed as a template.");return; }
552
+ string target=FolderDialog("Save a new PROJ folder here. Choose the SD-card root or a PC folder.",library);if(target==null)return;
553
+ Work(true,"Saving a new R16 project copy…");cancellation=new CancellationTokenSource();
554
+ try {
555
+ var snapshot=mixer.Snapshot(song,bits,native.Folder);var clips=Enumerable.Range(0,16).Select(i=>mixer.GetClip(i)).ToArray();
556
+ string path=await Task.Run(()=>native.SaveCopy(target,snapshot,clips,cancellation.Token));
557
+ Lcd("R16 COPY SAVED","Saved "+path+". Eject the card safely before returning the R16 to recorder mode. Original project preserved.");
558
+ }catch(Exception ex) { Lcd("SAVE PROJECT",ex.Message+" Original project unchanged; an incomplete destination copy may remain."); }
559
+ finally { cancellation.Dispose();cancellation=null;Work(false,notice.Text); }
560
+ }
561
+ private void SaveSession()
562
+ {
563
+ if(busy || recording)return;
564
+ var dialog=new Microsoft.Win32.SaveFileDialog { Title="Save Drum Buddy PC session",Filter="Drum Buddy session|*.dbuddy",DefaultExt=".dbuddy",FileName=sessionPath==null?"My song.dbuddy":sessionPath,InitialDirectory=Directory.Exists(library)?library:null };
565
+ if(dialog.ShowDialog(window)!=true)return;
566
+ try { var state=mixer.Snapshot(song,bits,native==null?"":native.Folder);using(var file=File.Create(dialog.FileName))new XmlSerializer(typeof(MixerSession)).Serialize(file,state);sessionPath=dialog.FileName;dirty=false;Lcd("SESSION SAVED","PC session saved. Keep its referenced audio files in place; use Save R16 project copy for a self-contained hardware project."); }catch(Exception ex) { Lcd("SAVE SESSION",ex.Message); }
567
+ }
568
+ private async Task OpenSession()
569
+ {
570
+ if(busy || recording)return;var dialog=new Microsoft.Win32.OpenFileDialog { Title="Open a Drum Buddy PC session",Filter="Drum Buddy session|*.dbuddy" };if(dialog.ShowDialog(window)!=true)return;
571
+ await LoadSession(dialog.FileName,false);
572
+ }
573
+ private async Task LoadSession(string path,bool automatic)
574
+ {
575
+ lock(mixer.Gate)mixer.Playing=false;audio.Dispose();Work(true,"Opening PC session…");cancellation=new CancellationTokenSource();
576
+ try {
577
+ MixerSession state;using(var file=File.OpenRead(path))state=(MixerSession)new XmlSerializer(typeof(MixerSession)).Deserialize(file);
578
+ if(state.Version!=1 || state.Tracks==null || state.Tracks.Length!=16)throw new IOException("Unsupported PC session version.");
579
+ foreach(var track in state.Tracks)MixerEngine.Validate(track);
580
+ var loaded=new Clip[16];int missing=0;
581
+ for(int i=0;i<16;i++)if(!string.IsNullOrEmpty(state.Tracks[i].File)) { string source=state.Tracks[i].File;if(!File.Exists(source)) { missing++;continue; }loaded[i]=await Task.Run(()=>Audio.Load(source,cache,cancellation.Token,null)); }
582
+ R16Project project=null;if(!string.IsNullOrEmpty(state.NativeProject) && File.Exists(Path.Combine(state.NativeProject,"PRJDATA.ZDT")))project=R16Project.Read(state.NativeProject);
583
+ for(int i=0;i<16;i++)mixer.Attach(i,loaded[i]);mixer.Apply(state);song=state.Name;bits=state.Bits;native=project;
584
+ mixer.Seek(0);if(!automatic)sessionPath=path;undo.Clear();dirty=false;
585
+ Lcd("SESSION LOADED",missing>0?"Session settings restored; "+missing+" audio file(s) are unavailable. Their assignments were retained.":automatic?"Your previous Full Control session has been restored.":"PC session loaded.");
586
+ }catch(Exception ex) { Lcd("OPEN SESSION",ex.Message); }finally { cancellation.Dispose();cancellation=null;Work(false,notice.Text);Sync(); }
587
+ }
588
+ private async Task ExportMix()
589
+ {
590
+ if(busy || recording)return;if(mixer.Duration<=0) { Lcd("NO AUDIO","Load audio tracks before saving a mix.");return; }
591
+ string folder=FolderDialog("Choose where to save the stereo WAV mix",exportFolder);if(folder==null)return;
592
+ lock(mixer.Gate)mixer.Playing=false;audio.Dispose();double previous=mixer.Position;bool loop=mixer.Loop;mixer.Loop=false;mixer.Seek(0);Work(true,"Saving stereo WAV mix…");cancellation=new CancellationTokenSource();
593
+ try {
594
+ string name="MIX"+DateTime.Now.ToString("HHmm",inv);string path=Path.Combine(folder,name+".WAV");int suffix=0;while(File.Exists(path))path=Path.Combine(folder,name.Substring(0,6)+(++suffix).ToString("00")+".WAV");
595
+ await Task.Run(delegate {
596
+ long remaining=(long)(mixer.Duration*Audio.Rate);var buffer=new float[8192*2];
597
+ using(var writer=new PcmWriter(path,bits,2)) { lock(mixer.Gate)mixer.Playing=true;while(remaining>0) { cancellation.Token.ThrowIfCancellationRequested();int frames=(int)Math.Min(8192,remaining);mixer.Read(buffer,0,frames*2);if(mixer.Error!=null)throw mixer.Error;writer.Write(buffer,frames*2,1);remaining-=frames; } }
598
+ });
599
+ Lcd("MIX SAVED","Saved "+path+" · "+bits+"-bit / 44.1 kHz. The PC mix includes pan and EQ, with a peak ceiling to prevent overload.");
600
+ }catch(Exception ex) { Lcd("SAVE MIX",ex.Message+" A partial mix may remain in the selected folder."); }
601
+ finally { lock(mixer.Gate)mixer.Playing=false;mixer.Loop=loop;mixer.Seek(previous);cancellation.Dispose();cancellation=null;Work(false,notice.Text);Sync(); }
602
+ }
603
+ private async Task StartTake()
604
+ {
605
+ if(busy || recording || finishing)return;
606
+ recordedTracks=Enumerable.Range(0,16).Where(i=>mixer.Tracks[i].Arm).ToArray();
607
+ bool recordMidi=noteArm.IsChecked==true;
608
+ if(recordedTracks.Length==0 && !recordMidi) { Lcd("ARM A TRACK","Arm an audio track in Track settings, or arm MIDI note recording in Open Menu > MIDI instrument.");return; }
609
+ if(recordMidi && !performance.Connected) { Lcd("CONNECT INSTRUMENT","Choose the instrument's MIDI input and click Connect instrument in Open Menu > MIDI instrument.");return; }
610
+ if(recordedTracks.Length>8 || recordedTracks.Select(i=>i%8).Distinct().Count()!=recordedTracks.Length) { Lcd("CHECK INPUTS","Arm up to eight tracks, using each physical input once. Tracks 1/9 share input 1, 2/10 input 2, etc.");return; }
611
+ if(recordedTracks.Length>0) { if(!EnsureAudio())return;if(!audio.CanRecord) { Lcd("SELECT R16","Select the Zoom R16 ASIO audio device in Open Menu > USB & settings.");return; } }
612
+ try {
613
+ takeFolder=Path.Combine(recordFolder,"Take_"+DateTime.Now.ToString("yyyyMMdd_HHmmss_fff",inv));
614
+ recordStart=mixer.Position;if(recordedTracks.Length>0)audio.BeginTake(takeFolder,bits,recordedTracks.Select(i=>i%8).ToArray());if(recordMidi)performance.Begin();recording=true;
615
+ lock(mixer.Gate) { mixer.Recording=audio.Connected;mixer.Playing=audio.Connected; }
616
+ outputDevice.IsEnabled=false;bits16.IsEnabled=false;bits24.IsEnabled=false;projectName.IsEnabled=false;arm.IsEnabled=false;noteArm.IsEnabled=false;noteInput.IsEnabled=false;noteOutput.IsEnabled=false;
617
+ Lcd("RECORDING",recordMidi?"Recording MIDI notes"+(recordedTracks.Length>0?" and armed R16 audio inputs":"")+". Stop saves the take.":"Recording armed R16 inputs. Faders and EQ affect playback; recorded inputs stay raw.");Sync();
618
+ }catch(Exception ex) { Lcd("RECORD",ex.Message); }
619
+ await Task.FromResult(0);
620
+ }
621
+ private async Task StopTake()
622
+ {
623
+ if(!recording || finishing)return;finishing=true;
624
+ lock(mixer.Gate) { mixer.Playing=false;mixer.Recording=false; }
625
+ notice.Text="Saving take…";
626
+ try {
627
+ string midiSaved=null;Exception midiError=null;
628
+ try { midiSaved=performance.Finish(takeFolder); }catch(Exception ex) { midiError=ex; }
629
+ TakeWriter take=await audio.EndTake();recording=false;
630
+ if(take!=null) {
631
+ foreach(int track in recordedTracks) {
632
+ string path=take.Paths[track%8];Clip clip=await Task.Run(()=>Audio.Load(path,cache,CancellationToken.None,null));mixer.Attach(track,clip);lock(mixer.Gate) { mixer.Tracks[track].Offset=recordStart;mixer.Tracks[track].Mute=false; }
633
+ }
634
+ Lcd("TAKE SAVED",take.Error==null?"New take saved and assigned to the armed PC tracks. Previous take files remain in My recordings.":take.Error.Message+" Any captured take files were kept.");
635
+ }
636
+ if(midiSaved!=null)Lcd("TAKE SAVED","Saved "+midiSaved+(take!=null?" and the armed audio WAV files.":". Use MIDI instrument to choose an output and play it."));
637
+ if(midiError!=null)Lcd("MIDI SAVE FAILED",midiError.Message+" Captured audio WAV files were still saved in "+takeFolder);
638
+ dirty=true;mixer.Seek(recordStart);
639
+ }catch(Exception ex) { recording=false;Lcd("TAKE FILES KEPT",ex.Message+" Recorded WAV files remain in "+recordFolder); }
640
+ finally { finishing=false;outputDevice.IsEnabled=true;bits16.IsEnabled=true;bits24.IsEnabled=true;projectName.IsEnabled=true;arm.IsEnabled=true;noteArm.IsEnabled=true;noteInput.IsEnabled=true;noteOutput.IsEnabled=true;Sync(); }
641
+ }
642
+ private async Task Leave()
643
+ {
644
+ if(leaving || disposed)return;
645
+ if(busy) { leaveAfterWork=true;if(cancellation!=null)cancellation.Cancel();notice.Text="Finishing the file operation, then returning home…";return; }
646
+ leaving=true;
647
+ try { if(recording)await StopTake();performance.StopPlayback();lock(mixer.Gate)mixer.Playing=false;SaveRecovery();audio.Dispose();midi.Dispose();timer.Stop();window.Hide();sidebar.Visibility=Visibility.Collapsed;menuButton.Content="Open Menu";if(returnHome!=null)returnHome(); }
648
+ finally { leaving=false; }
649
+ }
650
+ public void Dispose()
651
+ {
652
+ if(disposed)return;disposed=true;if(restoreAttempted)SaveRecovery();timer.Stop();performance.Dispose();audio.Dispose();midi.Dispose();mixer.Dispose();
653
+ if(window!=null) { window.Close();window=null; }
654
+ try { if(Directory.Exists(cache))Directory.Delete(cache,true); }catch { }
655
+ }
656
+ private void SaveRecovery()
657
+ {
658
+ lastRecovery=DateTime.Now;
659
+ try {
660
+ Directory.CreateDirectory(Path.GetDirectoryName(recoveryFile));
661
+ string temp=recoveryFile+".tmp";
662
+ using(var file=File.Create(temp))new XmlSerializer(typeof(MixerSession)).Serialize(file,mixer.Snapshot(song,bits,native==null?"":native.Folder));
663
+ if(File.Exists(recoveryFile))File.Replace(temp,recoveryFile,null);else File.Move(temp,recoveryFile);
664
+ dirty=false;
665
+ }catch(Exception ex) { if(notice!=null)notice.Text="Automatic session save failed: "+ex.Message+" Use Save PC session in Open Menu."; }
666
+ }
667
+ // An offscreen integration check exercises the same feedback/rendering path without
668
+ // opening an audio device, recording a microphone, or changing the user's windows.
669
+ internal void RenderFeedbackCheck(string path,Clip clip)
670
+ {
671
+ BuildWindow();mixer.Attach(0,clip);mixer.Tracks[0].Arm=true;statusMode=1;
672
+ mixer.Playing=true;var samples=new float[4410*2];mixer.Read(samples,0,samples.Length);
673
+ var inputs=new float[8*16];for(int f=0;f<16;f++)for(int c=0;c<8;c++)inputs[f*8+c]=(c+1)/8f;
674
+ audio.AccumulateInputPeaks(inputs,inputs.Length);meterSource.SelectedIndex=1;Sync();
675
+ // Tick normally skips hidden windows; the renderer uses the same meter/control methods.
676
+ float[] peaks=audio.DrainInputPeaks();
677
+ for(int i=0;i<8;i++) { mainMeters[i].Update(peaks[i],.075,"IN "+(i+1));inputClipLeds[i].State=peaks[i]>=.99?LampState.Red:LampState.Off; }
678
+ mainMeters[8].Update(mixer.MasterLeftPeak,.075,"L");mainMeters[9].Update(mixer.MasterRightPeak,.075,"R");
679
+ for(int i=0;i<16;i++)overviewMeters[i].Value=Math.Max(0,Math.Min(1,(20*Math.Log10(Math.Max(.00001,mixer.Peaks[i]))+60)/60));
680
+ if(statusLeds[0].State!=LampState.Red || mainMeters.Any(m=>m.Level.Db<=-60))throw new IOException("The rendered arm lamp or meter wiring failed.");
681
+ deviceText.Text="OFFSCREEN CHECK · generated samples · no hardware input";notice.Text="TEST: red arm lamp, green playback, eight input meters, stereo master meters. Generated test signals only.";
682
+ var visual=(FrameworkElement)window.Content;visual.Measure(new Size(1440,900));visual.Arrange(new Rect(0,0,1440,900));visual.UpdateLayout();
683
+ var bitmap=new RenderTargetBitmap(1440,900,96,96,PixelFormats.Pbgra32);bitmap.Render(visual);
684
+ var png=new PngBitmapEncoder();png.Frames.Add(BitmapFrame.Create(bitmap));using(var output=File.Create(path))png.Save(output);
685
+ }
686
+ }
687
+ }