@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.
Files changed (66) hide show
  1. package/LICENSE +23 -0
  2. package/NOTICE.txt +32 -0
  3. package/README.md +102 -2
  4. package/bin/flux.js +29 -0
  5. package/dependencies.json +14 -0
  6. package/package.json +60 -6
  7. package/payload/DrumBuddy-Setup.exe +0 -0
  8. package/scripts/install.js +58 -0
  9. package/scripts/version.js +12 -0
  10. package/source/AssetBootstrap.cs +49 -0
  11. package/source/Audio.cs +453 -0
  12. package/source/AudioDeviceSettings.cs +126 -0
  13. package/source/AudioInputs.cs +50 -0
  14. package/source/AudioOutputs.cs +327 -0
  15. package/source/AudioWorkspace.cs +71 -0
  16. package/source/BrandArtwork.cs +71 -0
  17. package/source/Build Instructions.txt +59 -0
  18. package/source/Build.ps1 +123 -0
  19. package/source/ControlMode.cs +687 -0
  20. package/source/ControlTests.cs +88 -0
  21. package/source/ConversionFormats.cs +59 -0
  22. package/source/ConversionTests.cs +66 -0
  23. package/source/DockablePane.cs +95 -0
  24. package/source/DriverSetup.cs +28 -0
  25. package/source/EditorView.cs +626 -0
  26. package/source/Fetch-Dependencies.ps1 +15 -0
  27. package/source/FloatRecording.cs +99 -0
  28. package/source/FloatRecordingTests.cs +170 -0
  29. package/source/Flux-Mark.svg +49 -0
  30. package/source/FluxTempo Logo Prompt.txt +3 -0
  31. package/source/FluxTempo-Mark.svg +49 -0
  32. package/source/HomeView.cs +32 -0
  33. package/source/InterfaceSettings.cs +219 -0
  34. package/source/InterfaceTests.cs +166 -0
  35. package/source/LICENSE.txt +23 -0
  36. package/source/Logo Prompt.txt +16 -0
  37. package/source/Main.cs +781 -0
  38. package/source/Main.xaml +292 -0
  39. package/source/MediaPlayerView.cs +274 -0
  40. package/source/MidiControl.cs +113 -0
  41. package/source/MidiPerformance.cs +125 -0
  42. package/source/Mixer.cs +280 -0
  43. package/source/NativeMemory.cs +165 -0
  44. package/source/PanelFeedback.cs +75 -0
  45. package/source/PlaybackTests.cs +134 -0
  46. package/source/Quick Start.txt +77 -0
  47. package/source/SelfTest.cs +78 -0
  48. package/source/SettingsWindow.cs +208 -0
  49. package/source/Setup.cs +192 -0
  50. package/source/Setup.xaml +28 -0
  51. package/source/ShellStyles.xaml +99 -0
  52. package/source/TempoGrid.cs +95 -0
  53. package/source/TempoGridTests.cs +108 -0
  54. package/source/Third Party Notices.txt +32 -0
  55. package/source/Timeline.cs +261 -0
  56. package/source/TimelineGestures.cs +57 -0
  57. package/source/Uninstall.cs +69 -0
  58. package/source/WorkspaceTests.cs +198 -0
  59. package/source/app.manifest +7 -0
  60. package/source/brand-archive/Drum-Buddy-Black-Hair.png +0 -0
  61. package/source/dependency-lock.json +62 -0
  62. package/source/fluxtempo-mark.png +0 -0
  63. package/source/logo.png +0 -0
  64. package/source/r16-panel.png +0 -0
  65. package/source/references/R16 Format Research.txt +16 -0
  66. package/test/package.test.js +55 -0
@@ -0,0 +1,113 @@
1
+ using System;
2
+ using System.Linq;
3
+ using System.IO;
4
+ using System.Collections.Generic;
5
+ using System.Runtime.InteropServices;
6
+
7
+ namespace DrumBuddy
8
+ {
9
+ public sealed class MidiPort
10
+ {
11
+ public int Id;
12
+ public string Name;
13
+ public override string ToString() { return Name; }
14
+ }
15
+ public sealed class McuAction
16
+ {
17
+ public string Kind;
18
+ public int Channel,Value;
19
+ }
20
+ public static class McuProtocol
21
+ {
22
+ public static McuAction Decode(int message)
23
+ {
24
+ int status=message&255,data1=(message>>8)&127,data2=(message>>16)&127;
25
+ if((status&240)==224 && (status&15)<=8) return new McuAction { Kind="fader",Channel=status&15,Value=(data1|(data2<<7))*127/16383 };
26
+ if((status&240)==176 && data1==60) return new McuAction { Kind="jog",Value=data2>=65?64-data2:data2<64?data2:0 };
27
+ if((status&240)==176 && data1>=16 && data1<=23) return new McuAction { Kind="pan",Channel=data1-16,Value=data2>=65?64-data2:data2<64?data2:0 };
28
+ if((status&240)!=144 || data2==0) return null;
29
+ if(data1<8) return new McuAction { Kind="arm",Channel=data1 };
30
+ if(data1<16) return new McuAction { Kind="solo",Channel=data1-8 };
31
+ if(data1<24) return new McuAction { Kind="mute",Channel=data1-16 };
32
+ if(data1<32) return new McuAction { Kind="select",Channel=data1-24 };
33
+ string kind=null; int value=0;
34
+ switch(data1) {
35
+ case 46: kind="bank";value=0;break; case 47: kind="bank";value=8;break;
36
+ case 91:kind="seek";value=-5;break;case 92:kind="seek";value=5;break;
37
+ case 93:kind="stop";break;case 94:kind="play";break;case 95:kind="record";break;
38
+ case 86:kind="loop";break;case 80:kind="save";break;case 81:kind="undo";break;
39
+ case 54:kind="loop";break;case 55:kind="loopend";break;
40
+ case 56:kind="previous";break;case 57:kind="next";break;case 58:kind="mark";break;
41
+ case 96:kind="parameter";value=-1;break;case 97:kind="parameter";value=1;break;
42
+ case 98:kind="track";value=-1;break;case 99:kind="track";value=1;break;
43
+ }
44
+ return kind==null?null:new McuAction { Kind=kind,Value=value };
45
+ }
46
+ }
47
+ internal sealed class R16Midi : IDisposable
48
+ {
49
+ [StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)] private struct InCaps { public ushort Mid,Pid;public uint Version;[MarshalAs(UnmanagedType.ByValTStr,SizeConst=32)] public string Name;public uint Support; }
50
+ [StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)] private struct OutCaps { public ushort Mid,Pid;public uint Version;[MarshalAs(UnmanagedType.ByValTStr,SizeConst=32)] public string Name;public ushort Technology,Voices,Notes,ChannelMask;public uint Support; }
51
+ private delegate void Callback(IntPtr handle,uint message,UIntPtr instance,UIntPtr param1,UIntPtr param2);
52
+ [DllImport("winmm.dll")] private static extern uint midiInGetNumDevs();
53
+ [DllImport("winmm.dll")] private static extern uint midiOutGetNumDevs();
54
+ [DllImport("winmm.dll",EntryPoint="midiInGetDevCapsW",CharSet=CharSet.Unicode)] private static extern uint midiInGetDevCaps(UIntPtr id,out InCaps caps,uint size);
55
+ [DllImport("winmm.dll",EntryPoint="midiOutGetDevCapsW",CharSet=CharSet.Unicode)] private static extern uint midiOutGetDevCaps(UIntPtr id,out OutCaps caps,uint size);
56
+ [DllImport("winmm.dll")] private static extern uint midiInOpen(out IntPtr handle,uint id,Callback callback,UIntPtr instance,uint flags);
57
+ [DllImport("winmm.dll")] private static extern uint midiInStart(IntPtr handle);
58
+ [DllImport("winmm.dll")] private static extern uint midiInStop(IntPtr handle);
59
+ [DllImport("winmm.dll")] private static extern uint midiInReset(IntPtr handle);
60
+ [DllImport("winmm.dll")] private static extern uint midiInClose(IntPtr handle);
61
+ [DllImport("winmm.dll")] private static extern uint midiOutOpen(out IntPtr handle,uint id,UIntPtr callback,UIntPtr instance,uint flags);
62
+ [DllImport("winmm.dll")] private static extern uint midiOutShortMsg(IntPtr handle,uint message);
63
+ [DllImport("winmm.dll")] private static extern uint midiOutClose(IntPtr handle);
64
+ private IntPtr input,output;
65
+ private readonly Callback callback;
66
+ public event Action<McuAction> Received;
67
+ public event Action<int> MessageReceived;
68
+ public long Messages;
69
+ public string Status="R16 MIDI not connected";
70
+ public bool Connected { get { return input!=IntPtr.Zero; } }
71
+ public bool HasOutput { get { return output!=IntPtr.Zero; } }
72
+ public static MidiPort[] Inputs() {
73
+ var ports=new List<MidiPort>();
74
+ for(uint i=0;i<midiInGetNumDevs();i++) { InCaps caps; if(midiInGetDevCaps((UIntPtr)i,out caps,(uint)Marshal.SizeOf(typeof(InCaps)))==0) ports.Add(new MidiPort { Id=(int)i,Name=caps.Name }); }
75
+ return ports.ToArray();
76
+ }
77
+ public static MidiPort[] Outputs() {
78
+ var ports=new List<MidiPort>();
79
+ for(uint i=0;i<midiOutGetNumDevs();i++) { OutCaps caps; if(midiOutGetDevCaps((UIntPtr)i,out caps,(uint)Marshal.SizeOf(typeof(OutCaps)))==0) ports.Add(new MidiPort { Id=(int)i,Name=caps.Name }); }
80
+ return ports.ToArray();
81
+ }
82
+ public R16Midi() { callback=OnMessage; }
83
+ private static void Check(uint result,string action) { if(result!=0) throw new IOException(action+" failed (MIDI "+result+"). Close other apps using the R16 MIDI port, then reconnect."); }
84
+ public void Connect(MidiPort inPort,MidiPort outPort)
85
+ {
86
+ Dispose(); Messages=0;
87
+ if(inPort==null && outPort==null) { Status="No MIDI port detected"; return; }
88
+ try {
89
+ if(inPort!=null)Check(midiInOpen(out input,(uint)inPort.Id,callback,UIntPtr.Zero,0x30000),"Opening MIDI input");
90
+ if(outPort!=null) { uint result=midiOutOpen(out output,(uint)outPort.Id,UIntPtr.Zero,UIntPtr.Zero,0); if(result!=0) output=IntPtr.Zero; }
91
+ if(input!=IntPtr.Zero)Check(midiInStart(input),"Starting MIDI input");
92
+ Status=input==IntPtr.Zero?"MIDI output connected":"MIDI connected · move a control or play a note to check";
93
+ } catch { Dispose(); throw; }
94
+ }
95
+ private void OnMessage(IntPtr handle,uint message,UIntPtr instance,UIntPtr param1,UIntPtr param2)
96
+ {
97
+ if(message!=0x3C3 && message!=0x3CC) return;
98
+ System.Threading.Interlocked.Increment(ref Messages);
99
+ var raw=MessageReceived;if(raw!=null) { try { raw(unchecked((int)param1.ToUInt64())); }catch { } }
100
+ McuAction action=McuProtocol.Decode(unchecked((int)param1.ToUInt64()));
101
+ var receive=Received;
102
+ if(action!=null && receive!=null) { try { receive(action); } catch { } }
103
+ }
104
+ public void Led(int note,bool on) { if(output!=IntPtr.Zero) midiOutShortMsg(output,(uint)(0x90|(note<<8)|((on?127:0)<<16))); }
105
+ public void Send(int message) { if(output!=IntPtr.Zero)midiOutShortMsg(output,unchecked((uint)message)); }
106
+ public void Dispose()
107
+ {
108
+ IntPtr old=input; input=IntPtr.Zero;
109
+ if(old!=IntPtr.Zero) { midiInStop(old);midiInReset(old);midiInClose(old); }
110
+ if(output!=IntPtr.Zero) { midiOutClose(output);output=IntPtr.Zero; }
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,125 @@
1
+ using System;
2
+ using System.IO;
3
+ using System.Linq;
4
+ using System.Text;
5
+ using System.Diagnostics;
6
+ using System.Threading;
7
+ using System.Threading.Tasks;
8
+ using System.Collections.Generic;
9
+
10
+ namespace DrumBuddy
11
+ {
12
+ public sealed class MidiMessage
13
+ {
14
+ public double Seconds;
15
+ public int Message;
16
+ }
17
+ public static class MidiFile
18
+ {
19
+ private sealed class RawEvent { public long Tick;public int Message,Tempo;public int Order; }
20
+ private static int Read16(BinaryReader r) { return (r.ReadByte()<<8)|r.ReadByte(); }
21
+ private static uint Read32(BinaryReader r) { return ((uint)r.ReadByte()<<24)|((uint)r.ReadByte()<<16)|((uint)r.ReadByte()<<8)|r.ReadByte(); }
22
+ private static void Write16(BinaryWriter w,int n) { w.Write((byte)(n>>8));w.Write((byte)n); }
23
+ private static void Write32(BinaryWriter w,long n) { w.Write((byte)(n>>24));w.Write((byte)(n>>16));w.Write((byte)(n>>8));w.Write((byte)n); }
24
+ private static long Vlq(BinaryReader r) { long value=0;for(int i=0;i<4;i++) { byte b=r.ReadByte();value=(value<<7)|(uint)(b&127);if((b&128)==0)return value; }throw new IOException("Invalid MIDI event time."); }
25
+ private static void Vlq(BinaryWriter w,long value) { if(value<0 || value>0x0FFFFFFF)throw new IOException("MIDI event time is too long.");var bytes=new byte[4];int i=3;bytes[i]=(byte)(value&127);while((value>>=7)>0)bytes[--i]=(byte)((value&127)|128);w.Write(bytes,i,4-i); }
26
+ public static void Save(string path,IEnumerable<MidiMessage> messages,double endSeconds)
27
+ {
28
+ using(var memory=new MemoryStream()) {
29
+ var track=new BinaryWriter(memory);Vlq(track,0);track.Write(new byte[]{255,81,3,7,161,32}); // 120 BPM, 480 ticks/quarter.
30
+ long previous=0;
31
+ foreach(var e in messages.OrderBy(e=>e.Seconds)) {
32
+ int status=e.Message&255;if(status<128 || status>=240)continue;
33
+ long tick=(long)Math.Round(Math.Max(0,e.Seconds)*960);Vlq(track,tick-previous);previous=tick;
34
+ track.Write((byte)status);track.Write((byte)((e.Message>>8)&127));if((status&240)!=192 && (status&240)!=208)track.Write((byte)((e.Message>>16)&127));
35
+ }
36
+ Vlq(track,Math.Max(previous,(long)Math.Round(endSeconds*960))-previous);track.Write(new byte[]{255,47,0});track.Flush();
37
+ using(var file=new FileStream(path,FileMode.CreateNew,FileAccess.Write))using(var output=new BinaryWriter(file)) { output.Write(Encoding.ASCII.GetBytes("MThd"));Write32(output,6);Write16(output,0);Write16(output,1);Write16(output,480);output.Write(Encoding.ASCII.GetBytes("MTrk"));Write32(output,memory.Length);output.Write(memory.ToArray()); }
38
+ }
39
+ }
40
+ public static MidiMessage[] Load(string path)
41
+ {
42
+ if(new FileInfo(path).Length>32*1024*1024)throw new IOException("This MIDI file is too large.");
43
+ var events=new List<RawEvent>();int division,format,trackCount,order=0;
44
+ using(var stream=File.OpenRead(path))using(var r=new BinaryReader(stream)) {
45
+ if(Encoding.ASCII.GetString(r.ReadBytes(4))!="MThd")throw new IOException("This is not a Standard MIDI file.");
46
+ uint header=Read32(r);if(header<6 || header>1024)throw new IOException("Invalid MIDI header.");format=Read16(r);trackCount=Read16(r);division=Read16(r);stream.Position+=header-6;
47
+ if(format>1 || trackCount<1 || trackCount>1024 || division==0)throw new IOException("Use a type 0 or type 1 Standard MIDI file.");
48
+ for(int t=0;t<trackCount;t++) {
49
+ if(Encoding.ASCII.GetString(r.ReadBytes(4))!="MTrk")throw new IOException("Missing MIDI track.");uint length=Read32(r);long end=stream.Position+length;if(end>stream.Length)throw new IOException("Truncated MIDI track.");
50
+ long tick=0;int running=0;
51
+ while(stream.Position<end) {
52
+ tick+=Vlq(r);int first=r.ReadByte(),status;
53
+ if(first<128) { if(running==0)throw new IOException("Invalid MIDI running status.");status=running;stream.Position--; }else status=first;
54
+ if(status==255) { int type=r.ReadByte();long size=Vlq(r);if(size>end-stream.Position)throw new IOException("Truncated MIDI metadata.");if(type==81 && size==3) { int tempo=(r.ReadByte()<<16)|(r.ReadByte()<<8)|r.ReadByte();if(tempo<=0)throw new IOException("Invalid MIDI tempo.");events.Add(new RawEvent { Tick=tick,Tempo=tempo,Order=order++ }); }else stream.Position+=size; }
55
+ else if(status==240 || status==247) { long size=Vlq(r);if(size>end-stream.Position)throw new IOException("Truncated MIDI system event.");stream.Position+=size;running=0; }
56
+ else if(status>=128 && status<240) { running=status;int d1=r.ReadByte(),d2=(status&240)==192 || (status&240)==208?0:r.ReadByte();if(d1>127 || d2>127)throw new IOException("Invalid MIDI channel data.");events.Add(new RawEvent { Tick=tick,Message=status|(d1<<8)|(d2<<16),Order=order++ }); }
57
+ else throw new IOException("Unsupported MIDI event.");
58
+ if(stream.Position>end || events.Count>1000000)throw new IOException("Invalid or oversized MIDI track.");
59
+ }
60
+ }
61
+ }
62
+ var result=new List<MidiMessage>();double seconds=0;long last=0;int currentTempo=500000;
63
+ double smpte=0;if((division&0x8000)!=0) { int fps=-(sbyte)(division>>8),sub=division&255;if(sub==0 || (fps!=24 && fps!=25 && fps!=29 && fps!=30))throw new IOException("Invalid SMPTE MIDI timing.");smpte=(fps==29?29.97:fps)*sub; }
64
+ foreach(var e in events.OrderBy(e=>e.Tick).ThenBy(e=>e.Tempo>0?0:1).ThenBy(e=>e.Order)) {
65
+ seconds+=(e.Tick-last)*(smpte>0?1/smpte:currentTempo/(1000000.0*division));last=e.Tick;
66
+ if(e.Tempo>0)currentTempo=e.Tempo;else result.Add(new MidiMessage { Seconds=seconds,Message=e.Message });
67
+ }
68
+ return result.ToArray();
69
+ }
70
+ }
71
+ internal sealed class MidiPerformance : IDisposable
72
+ {
73
+ private readonly R16Midi port=new R16Midi();
74
+ private readonly object gate=new object();
75
+ private readonly Stopwatch recordingClock=new Stopwatch();
76
+ private readonly List<MidiMessage> captured=new List<MidiMessage>();
77
+ private readonly HashSet<int> held=new HashSet<int>();
78
+ private CancellationTokenSource playback;
79
+ public bool Recording,Playing;
80
+ public string LastEvent="No MIDI notes received",LoadedPath;
81
+ public long NoteCount;
82
+ public Exception Error;
83
+ private MidiMessage[] song=new MidiMessage[0];
84
+ public bool Connected { get { return port.Connected; } }
85
+ public double RecordSeconds { get { return recordingClock.Elapsed.TotalSeconds; } }
86
+ public MidiPerformance() { port.MessageReceived+=Accept; }
87
+ public void Connect(MidiPort input,MidiPort output) { StopPlayback();if(Recording)throw new IOException("Stop the MIDI recording before changing ports.");port.Connect(input,output); }
88
+ internal void Accept(int message)
89
+ {
90
+ int status=message&255,type=status&240,d1=(message>>8)&127,d2=(message>>16)&127;
91
+ if(status<128 || status>=240)return;
92
+ lock(gate) {
93
+ if(type==144 && d2>0) { NoteCount++;LastEvent="Ch "+((status&15)+1)+" · note "+d1+" · velocity "+d2; }
94
+ else LastEvent="Ch "+((status&15)+1)+" · "+(type==128 || (type==144 && d2==0)?"note off "+d1:type==176?"CC "+d1+" = "+d2:"MIDI "+type.ToString("X2"));
95
+ if(!Recording)return;
96
+ if(captured.Count>=1000000) { Error=new IOException("The MIDI take reached its event limit. Stop and save it.");return; }
97
+ captured.Add(new MidiMessage { Seconds=recordingClock.Elapsed.TotalSeconds,Message=message&0xFFFFFF });
98
+ int key=((status&15)<<8)|d1;if(type==144 && d2>0)held.Add(key);else if(type==128 || (type==144 && d2==0))held.Remove(key);
99
+ }
100
+ }
101
+ public void Begin()
102
+ {
103
+ StopPlayback();lock(gate) { captured.Clear();held.Clear();Error=null;recordingClock.Restart();Recording=true; }
104
+ }
105
+ public string Finish(string folder)
106
+ {
107
+ MidiMessage[] events;double end;
108
+ lock(gate) { if(!Recording)return null;recordingClock.Stop();end=recordingClock.Elapsed.TotalSeconds;Recording=false;foreach(int key in held)captured.Add(new MidiMessage { Seconds=end,Message=(128|(key>>8))|((key&127)<<8) });held.Clear();events=captured.ToArray(); }
109
+ Directory.CreateDirectory(folder);string path=Path.Combine(folder,"PERFORM.MID");int i=0;while(File.Exists(path))path=Path.Combine(folder,"PERF"+(++i).ToString("0000")+".MID");MidiFile.Save(path,events,end);LoadedPath=path;song=events;return path;
110
+ }
111
+ public void Load(string path) { StopPlayback();var data=MidiFile.Load(path);song=data;LoadedPath=path; }
112
+ public async Task Play()
113
+ {
114
+ if(Recording)throw new IOException("Stop recording before MIDI playback.");if(!port.HasOutput)throw new IOException("Choose a MIDI output and click Connect instrument first.");if(song.Length==0)throw new IOException("Open or record a MIDI file first.");
115
+ StopPlayback();var cancel=new CancellationTokenSource();playback=cancel;Playing=true;var clock=Stopwatch.StartNew();
116
+ try {
117
+ foreach(var e in song) { while(clock.Elapsed.TotalSeconds<e.Seconds) { cancel.Token.ThrowIfCancellationRequested();await Task.Delay(Math.Max(1,Math.Min(10,(int)((e.Seconds-clock.Elapsed.TotalSeconds)*1000))),cancel.Token).ConfigureAwait(false); }lock(gate) { cancel.Token.ThrowIfCancellationRequested();if(playback==cancel)port.Send(e.Message); } }
118
+ }catch(OperationCanceledException) { }
119
+ finally { lock(gate) { if(playback==cancel) { AllNotesOff();Playing=false;playback=null; } }cancel.Dispose(); }
120
+ }
121
+ private void AllNotesOff() { for(int c=0;c<16;c++)port.Send(176|c|(123<<8)); }
122
+ public void StopPlayback() { lock(gate) { var cancel=playback;playback=null;if(cancel!=null) { try { cancel.Cancel(); }catch(ObjectDisposedException) { } }Playing=false;AllNotesOff(); } }
123
+ public void Dispose() { StopPlayback();port.Dispose(); }
124
+ }
125
+ }
@@ -0,0 +1,280 @@
1
+ using System;
2
+ using System.IO;
3
+ using System.Linq;
4
+ using System.Collections.Generic;
5
+ using System.Collections.Concurrent;
6
+ using System.Threading;
7
+ using System.Threading.Tasks;
8
+ using System.Xml.Serialization;
9
+ using NAudio.Wave;
10
+ using NAudio.Wave.SampleProviders;
11
+ using NAudio.Dsp;
12
+
13
+ namespace DrumBuddy
14
+ {
15
+ public sealed class TrackState
16
+ {
17
+ public string Name = "", File = "";
18
+ public double Offset;
19
+ public int Fader = 100, Pan, HighGain, MidGain, LowGain;
20
+ public int HighFreq = 8000, MidFreq = 1000, LowFreq = 125;
21
+ public double MidQ = .5;
22
+ public bool Mute, Solo, Arm, Stereo, Invert;
23
+ public bool HighOn = true, MidOn = true, LowOn = true, ReverbOn = true, ChorusOn = true;
24
+ public int Reverb, Chorus;
25
+ public TrackState Copy() { return (TrackState)MemberwiseClone(); }
26
+ }
27
+ public sealed class MixerSession
28
+ {
29
+ public int Version = 1, Bits = 24, Master = 100;
30
+ public string Name = "NEW SONG", NativeProject = "";
31
+ public TrackState[] Tracks;
32
+ }
33
+ internal sealed class MixerVoice : IDisposable
34
+ {
35
+ public Clip Clip;
36
+ public WaveFileReader Reader;
37
+ public ISampleProvider Samples;
38
+ public float[] Buffer = new float[0];
39
+ public BiQuadFilter[] Left, Right;
40
+ public string EqKey;
41
+ public MixerVoice(Clip clip) { Clip = clip; Reader = new WaveFileReader(clip.Cache); Samples = Reader.ToSampleProvider(); }
42
+ public void Filters(TrackState state, bool reset)
43
+ {
44
+ string key = string.Join("/",new object[] { state.LowOn,state.LowGain,state.LowFreq,state.MidOn,state.MidGain,state.MidFreq,state.MidQ,state.HighOn,state.HighGain,state.HighFreq });
45
+ if (!reset && key == EqKey) return;
46
+ EqKey = key;
47
+ Left = CreateFilters(state); Right = CreateFilters(state);
48
+ }
49
+ private static BiQuadFilter[] CreateFilters(TrackState s)
50
+ {
51
+ var filters = new List<BiQuadFilter>();
52
+ if (s.LowOn && s.LowGain != 0) filters.Add(BiQuadFilter.LowShelf(Audio.Rate,s.LowFreq,1,s.LowGain));
53
+ if (s.MidOn && s.MidGain != 0) filters.Add(BiQuadFilter.PeakingEQ(Audio.Rate,s.MidFreq,(float)s.MidQ,s.MidGain));
54
+ if (s.HighOn && s.HighGain != 0) filters.Add(BiQuadFilter.HighShelf(Audio.Rate,s.HighFreq,1,s.HighGain));
55
+ return filters.ToArray();
56
+ }
57
+ public void Dispose() { Reader.Dispose(); }
58
+ }
59
+ // All stream operations and state edits share one short lock. Audio stays on the audio callback.
60
+ public sealed class MixerEngine : ISampleProvider, IDisposable
61
+ {
62
+ public readonly object Gate = new object();
63
+ public readonly TrackState[] Tracks = new TrackState[16];
64
+ private readonly MixerVoice[] voices = new MixerVoice[16];
65
+ public readonly float[] Peaks = new float[16];
66
+ public float MasterPeak,MasterLeftPeak,MasterRightPeak;
67
+ public bool Playing, Recording, Loop;
68
+ public double LoopStart, LoopEnd;
69
+ public int Master = 100;
70
+ private long frame;
71
+ public Exception Error;
72
+ public MixerEngine() { for (int i=0;i<16;i++) Tracks[i] = new TrackState(); }
73
+ public WaveFormat WaveFormat { get { return WaveFormat.CreateIeeeFloatWaveFormat(Audio.Rate,2); } }
74
+ public double Position { get { lock(Gate) return frame / (double)Audio.Rate; } }
75
+ public double Duration { get { lock(Gate) return DurationUnsafe(); } }
76
+ private double DurationUnsafe() { double end=0; for(int i=0;i<16;i++) if(voices[i]!=null) end=Math.Max(end,Tracks[i].Offset+voices[i].Clip.Duration); return end; }
77
+ public bool HasAudio(int index) { lock(Gate) return voices[index] != null; }
78
+ public Clip GetClip(int index) { lock(Gate) return voices[index] == null ? null : voices[index].Clip; }
79
+ public static double FaderDb(int value)
80
+ {
81
+ if(value<=0)return -120;
82
+ int[] positions={1,7,14,36,60,80,100,114,127};
83
+ double[] levels={-90,-60,-40,-20,-10,-5,0,6,12};
84
+ for(int i=1;i<positions.Length;i++)if(value<=positions[i])return levels[i-1]+(levels[i]-levels[i-1])*(value-positions[i-1])/(positions[i]-positions[i-1]);
85
+ return 12;
86
+ }
87
+ public static double FaderGain(int value) { return value <= 0 ? 0 : Math.Pow(10,FaderDb(value)/20); }
88
+ public void Attach(int index, Clip clip)
89
+ {
90
+ var voice = clip == null ? null : new MixerVoice(clip);
91
+ lock(Gate) {
92
+ if(voices[index]!=null) voices[index].Dispose();
93
+ voices[index]=voice;
94
+ if(clip!=null) { Tracks[index].File=clip.Source; Tracks[index].Name=Path.GetFileNameWithoutExtension(clip.Name); }
95
+ else { Tracks[index].File=""; Tracks[index].Name=""; Tracks[index].Offset=0; }
96
+ }
97
+ }
98
+ public void Seek(double seconds) { lock(Gate) { frame=(long)(Math.Max(0,seconds)*Audio.Rate); foreach(var voice in voices) if(voice!=null) voice.EqKey=null; } }
99
+ public MixerSession Snapshot(string name, int bits, string native)
100
+ {
101
+ lock(Gate) return new MixerSession { Name=name,Bits=bits,Master=Master,NativeProject=native,Tracks=Tracks.Select(t=>t.Copy()).ToArray() };
102
+ }
103
+ public void Apply(MixerSession session)
104
+ {
105
+ if(session==null || session.Tracks==null || session.Tracks.Length!=16) throw new IOException("This is not a 16-track Drum Buddy session.");
106
+ lock(Gate) { for(int i=0;i<16;i++) { Validate(session.Tracks[i]); Tracks[i]=session.Tracks[i].Copy(); } Master=Math.Max(0,Math.Min(127,session.Master)); }
107
+ }
108
+ public static void Validate(TrackState t)
109
+ {
110
+ if(t==null || t.Fader<0 || t.Fader>127 || t.Pan<-50 || t.Pan>50 || t.Offset<0 || t.Offset>86400 || double.IsNaN(t.Offset) || double.IsInfinity(t.Offset) ||
111
+ t.HighGain<-12 || t.HighGain>12 || t.MidGain<-12 || t.MidGain>12 || t.LowGain<-12 || t.LowGain>12 ||
112
+ t.HighFreq<40 || t.HighFreq>18000 || t.MidFreq<40 || t.MidFreq>18000 || t.LowFreq<40 || t.LowFreq>18000 ||
113
+ t.MidQ<.1 || t.MidQ>16 || double.IsNaN(t.MidQ) || t.Reverb<0 || t.Reverb>100 || t.Chorus<0 || t.Chorus>100)
114
+ throw new IOException("The project contains unsupported track settings.");
115
+ }
116
+ public int Read(float[] buffer,int offset,int count)
117
+ {
118
+ Array.Clear(buffer,offset,count);
119
+ lock(Gate) {
120
+ if(!Playing) return count;
121
+ try {
122
+ int remaining=count/2, target=offset;
123
+ while(remaining>0) {
124
+ long end=(long)(DurationUnsafe()*Audio.Rate);
125
+ bool looping=Loop && !Recording && LoopEnd>LoopStart+.01;
126
+ long loopEnd=(long)(LoopEnd*Audio.Rate);
127
+ if(looping && frame>=loopEnd) { frame=(long)(LoopStart*Audio.Rate); foreach(var v in voices) if(v!=null) v.EqKey=null; }
128
+ if(!Recording && !looping && frame>=end) { Playing=false; break; }
129
+ int frames=remaining;
130
+ if(looping) frames=(int)Math.Min(frames,loopEnd-frame);
131
+ else if(!Recording) frames=(int)Math.Min(frames,end-frame);
132
+ if(frames<=0) break;
133
+ MixBlock(buffer,target,frames);
134
+ frame+=frames; remaining-=frames; target+=frames*2;
135
+ }
136
+ } catch(Exception ex) { Error=ex; Playing=false; }
137
+ return count;
138
+ }
139
+ }
140
+ private void MixBlock(float[] output,int target,int frames)
141
+ {
142
+ bool solo=Tracks.Any(t=>t.Solo);
143
+ for(int index=0;index<16;index++) {
144
+ MixerVoice v=voices[index]; TrackState s=Tracks[index];
145
+ if(v==null || s.Mute || (solo && !s.Solo)) continue;
146
+ long clipFrame=frame-(long)(s.Offset*Audio.Rate); int skip=clipFrame<0 ? (int)Math.Min(frames,-clipFrame) : 0;
147
+ clipFrame=Math.Max(0,clipFrame); if(skip==frames || clipFrame>=v.Reader.SampleCount) continue;
148
+ int channels=v.Reader.WaveFormat.Channels;
149
+ long position=clipFrame*v.Reader.WaveFormat.BlockAlign;
150
+ bool seek=v.Reader.Position!=position;
151
+ if(seek) v.Reader.Position=Math.Min(v.Reader.Length,position);
152
+ v.Filters(s,seek || v.EqKey==null);
153
+ int needed=(frames-skip)*channels;
154
+ if(v.Buffer.Length<needed) v.Buffer=new float[needed];
155
+ int read=v.Samples.Read(v.Buffer,0,needed), actual=read/channels;
156
+ double gain=FaderGain(s.Fader)*(s.Invert?-1:1);
157
+ double pan=s.Pan/50.0;
158
+ double leftGain=gain*(pan>0?1-pan:1), rightGain=gain*(pan<0?1+pan:1);
159
+ float peak=0;
160
+ for(int f=0;f<actual;f++) {
161
+ float l=v.Buffer[f*channels],r=channels==1?l:v.Buffer[f*channels+1];
162
+ foreach(var eq in v.Left) l=eq.Transform(l);
163
+ foreach(var eq in v.Right) r=eq.Transform(r);
164
+ l*=(float)leftGain; r*=(float)rightGain;
165
+ peak=Math.Max(peak,Math.Max(Math.Abs(l),Math.Abs(r)));
166
+ output[target+(f+skip)*2]+=l; output[target+(f+skip)*2+1]+=r;
167
+ }
168
+ Peaks[index]=Math.Max(Peaks[index],peak);
169
+ }
170
+ float master=(float)FaderGain(Master);
171
+ for(int i=target;i<target+frames*2;i++) {
172
+ float sample=output[i]*master; MasterPeak=Math.Max(MasterPeak,Math.Abs(sample));
173
+ if((i-target)%2==0)MasterLeftPeak=Math.Max(MasterLeftPeak,Math.Abs(sample));else MasterRightPeak=Math.Max(MasterRightPeak,Math.Abs(sample));
174
+ // Protect headphones from summing overload; meters retain the pre-ceiling peak.
175
+ output[i]=Math.Max(-.98855f,Math.Min(.98855f,sample));
176
+ }
177
+ }
178
+ public void Dispose() { lock(Gate) { Playing=false; for(int i=0;i<16;i++) { if(voices[i]!=null) voices[i].Dispose(); voices[i]=null; } } }
179
+ }
180
+
181
+ internal sealed class TakeWriter
182
+ {
183
+ private readonly BlockingCollection<float[]> queue=new BlockingCollection<float[]>(128);
184
+ private readonly ConcurrentQueue<float[]> pool=new ConcurrentQueue<float[]>();
185
+ private readonly List<PcmWriter> writers=new List<PcmWriter>();
186
+ private readonly int[] inputs;
187
+ private readonly Task task;
188
+ public readonly Dictionary<int,string> Paths=new Dictionary<int,string>();
189
+ public Exception Error;
190
+ public long Frames;
191
+ public TakeWriter(string folder,int bits,int[] selected)
192
+ {
193
+ inputs=selected.Distinct().OrderBy(i=>i).ToArray();
194
+ if(inputs.Length==0 || inputs.Any(i=>i<0 || i>7)) throw new IOException("Arm at least one input track before recording.");
195
+ Directory.CreateDirectory(folder);
196
+ try { foreach(int input in inputs) { string path=Path.Combine(folder,"INPUT_"+(input+1)+".WAV"); writers.Add(new PcmWriter(path,bits,1)); Paths.Add(input,path); } }
197
+ catch { foreach(var writer in writers) writer.Dispose(); queue.Dispose(); throw; }
198
+ task=Task.Run((Action)WriteLoop);
199
+ }
200
+ public void Add(AsioAudioAvailableEventArgs e)
201
+ {
202
+ if(Error!=null) return;
203
+ int count=e.SamplesPerBuffer*8; float[] block;
204
+ if(!pool.TryDequeue(out block) || block.Length!=count) block=new float[count];
205
+ try { e.GetAsInterleavedSamples(block); if(!queue.TryAdd(block)) throw new IOException("The disk could not keep up. The captured take has been kept."); Interlocked.Add(ref Frames,e.SamplesPerBuffer); }
206
+ catch(Exception ex) { Error=ex; }
207
+ }
208
+ internal void AddSamples(float[] samples,int frames)
209
+ {
210
+ if(Error!=null)return;
211
+ int count=frames*8;if(frames<1 || samples.Length<count)throw new ArgumentException("Expected eight interleaved input channels.");
212
+ float[] block;if(!pool.TryDequeue(out block) || block.Length!=count)block=new float[count];Array.Copy(samples,block,count);
213
+ if(!queue.TryAdd(block))Error=new IOException("The disk could not keep up.");else Interlocked.Add(ref Frames,frames);
214
+ }
215
+ private void WriteLoop()
216
+ {
217
+ try {
218
+ float[] mono=new float[0];
219
+ foreach(float[] block in queue.GetConsumingEnumerable()) {
220
+ int frames=block.Length/8; if(mono.Length<frames) mono=new float[frames];
221
+ for(int w=0;w<inputs.Length;w++) { for(int f=0;f<frames;f++) mono[f]=block[f*8+inputs[w]]; writers[w].Write(mono,frames,1); }
222
+ pool.Enqueue(block);
223
+ }
224
+ } catch(Exception ex) { Error=ex; }
225
+ }
226
+ public async Task Finish() { queue.CompleteAdding(); await task; foreach(var writer in writers) { try { writer.Dispose(); } catch(Exception ex) { Error=ex; } } queue.Dispose(); }
227
+ }
228
+ internal sealed class ControlAudio : IDisposable
229
+ {
230
+ private IWavePlayer device;
231
+ private PlaybackRoute playbackRoute;
232
+ private AsioOut asio;
233
+ private readonly object gate=new object();
234
+ private TakeWriter take;
235
+ public readonly float[] InputPeaks=new float[8];
236
+ private float[] inputBuffer=new float[0];
237
+ public Exception Error;
238
+ public bool Connected { get { return device!=null; } }
239
+ public bool CanRecord { get { return asio!=null; } }
240
+ public float[] DrainInputPeaks() { lock(gate) { var result=(float[])InputPeaks.Clone();Array.Clear(InputPeaks,0,InputPeaks.Length);return result; } }
241
+ internal void AccumulateInputPeaks(float[] samples,int count) { lock(gate) { for(int c=0;c<8;c++) { float peak=0;for(int f=c;f<count;f+=8)peak=Math.Max(peak,Math.Abs(samples[f]));InputPeaks[c]=Math.Max(InputPeaks[c],peak); } } }
242
+ public Exception TakeError { get { lock(gate) return take==null?null:take.Error; } }
243
+ public void Connect(MixerEngine engine,OutputChoice output)
244
+ {
245
+ Dispose(); Error=null;
246
+ try {
247
+ if(output!=null && output.Driver!=null) {
248
+ asio=new AsioOut(output.Driver); device=asio;
249
+ if(asio.DriverInputChannelCount<8) throw new IOException("Full Control needs the R16's eight ASIO inputs.");
250
+ if(!asio.IsSampleRateSupported(Audio.Rate)) throw new IOException("The device must support 44.1 kHz.");
251
+ asio.InitRecordAndPlayback(new SampleToWaveProvider(engine),8,Audio.Rate);
252
+ asio.AudioAvailable+=delegate(object sender,AsioAudioAvailableEventArgs e) {
253
+ lock(gate) {
254
+ try {
255
+ int count=e.SamplesPerBuffer*8;
256
+ if(inputBuffer.Length<count) inputBuffer=new float[count];
257
+ e.GetAsInterleavedSamples(inputBuffer);
258
+ AccumulateInputPeaks(inputBuffer,count);
259
+ if(take!=null) take.Add(e);
260
+ } catch(Exception ex) { Error=ex; }
261
+ }
262
+ };
263
+ } else {
264
+ playbackRoute=AudioOutputs.Open(engine,output);device=playbackRoute.Player;
265
+ }
266
+ var active=device;
267
+ device.PlaybackStopped+=delegate(object sender,StoppedEventArgs e) { if(device==active)Error=e.Exception??new IOException("The audio interface stopped. Check its USB connection."); };
268
+ device.Play();
269
+ } catch { Dispose(); throw; }
270
+ }
271
+ public void BeginTake(string folder,int bits,int[] inputs) { if(asio==null) throw new IOException("Select the Zoom ASIO audio device to record."); var writer=new TakeWriter(folder,bits,inputs); lock(gate) take=writer; }
272
+ public async Task<TakeWriter> EndTake() { TakeWriter writer; lock(gate) { writer=take; take=null; } if(writer!=null) await writer.Finish(); return writer; }
273
+ public void Dispose()
274
+ {
275
+ var old=device; device=null; asio=null;
276
+ if(playbackRoute!=null) { playbackRoute.Dispose();playbackRoute=null; }
277
+ else if(old!=null) { try { old.Stop(); } catch { } try { old.Dispose(); } catch { } }
278
+ }
279
+ }
280
+ }