@pro-gram/flux 0.0.0-stage → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/NOTICE.txt +32 -0
- package/README.md +102 -2
- package/bin/flux.js +29 -0
- package/dependencies.json +14 -0
- package/package.json +60 -6
- package/payload/DrumBuddy-Setup.exe +0 -0
- package/scripts/install.js +58 -0
- package/scripts/version.js +12 -0
- package/source/AssetBootstrap.cs +49 -0
- package/source/Audio.cs +453 -0
- package/source/AudioDeviceSettings.cs +126 -0
- package/source/AudioInputs.cs +50 -0
- package/source/AudioOutputs.cs +327 -0
- package/source/AudioWorkspace.cs +71 -0
- package/source/BrandArtwork.cs +71 -0
- package/source/Build Instructions.txt +59 -0
- package/source/Build.ps1 +123 -0
- package/source/ControlMode.cs +687 -0
- package/source/ControlTests.cs +88 -0
- package/source/ConversionFormats.cs +59 -0
- package/source/ConversionTests.cs +66 -0
- package/source/DockablePane.cs +95 -0
- package/source/DriverSetup.cs +28 -0
- package/source/EditorView.cs +626 -0
- package/source/Fetch-Dependencies.ps1 +15 -0
- package/source/FloatRecording.cs +99 -0
- package/source/FloatRecordingTests.cs +170 -0
- package/source/Flux-Mark.svg +49 -0
- package/source/FluxTempo Logo Prompt.txt +3 -0
- package/source/FluxTempo-Mark.svg +49 -0
- package/source/HomeView.cs +32 -0
- package/source/InterfaceSettings.cs +219 -0
- package/source/InterfaceTests.cs +166 -0
- package/source/LICENSE.txt +23 -0
- package/source/Logo Prompt.txt +16 -0
- package/source/Main.cs +781 -0
- package/source/Main.xaml +292 -0
- package/source/MediaPlayerView.cs +274 -0
- package/source/MidiControl.cs +113 -0
- package/source/MidiPerformance.cs +125 -0
- package/source/Mixer.cs +280 -0
- package/source/NativeMemory.cs +165 -0
- package/source/PanelFeedback.cs +75 -0
- package/source/PlaybackTests.cs +134 -0
- package/source/Quick Start.txt +77 -0
- package/source/SelfTest.cs +78 -0
- package/source/SettingsWindow.cs +208 -0
- package/source/Setup.cs +192 -0
- package/source/Setup.xaml +28 -0
- package/source/ShellStyles.xaml +99 -0
- package/source/TempoGrid.cs +95 -0
- package/source/TempoGridTests.cs +108 -0
- package/source/Third Party Notices.txt +32 -0
- package/source/Timeline.cs +261 -0
- package/source/TimelineGestures.cs +57 -0
- package/source/Uninstall.cs +69 -0
- package/source/WorkspaceTests.cs +198 -0
- package/source/app.manifest +7 -0
- package/source/brand-archive/Drum-Buddy-Black-Hair.png +0 -0
- package/source/dependency-lock.json +62 -0
- package/source/fluxtempo-mark.png +0 -0
- package/source/logo.png +0 -0
- package/source/r16-panel.png +0 -0
- package/source/references/R16 Format Research.txt +16 -0
- package/test/package.test.js +55 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Text;
|
|
5
|
+
using System.Text.RegularExpressions;
|
|
6
|
+
using System.Threading;
|
|
7
|
+
using NAudio.Wave;
|
|
8
|
+
|
|
9
|
+
namespace DrumBuddy
|
|
10
|
+
{
|
|
11
|
+
// Independent binary reader/writer. Format research: zoomrlib 1.1.0 by
|
|
12
|
+
// Remy Taymans and Glenn Boysko, https://gitlab.com/remytms/zoomrlib.
|
|
13
|
+
// Retain the original container; patch only identified fields. Do not synthesize firmware state.
|
|
14
|
+
public sealed class R16Project
|
|
15
|
+
{
|
|
16
|
+
public const int ProjectSize=3332;
|
|
17
|
+
public static readonly int[] Frequencies={40,50,63,80,100,125,160,200,250,315,400,500,630,800,1000,1300,1600,2000,2500,3200,4000,5000,6300,8000,10000,12500,16000,18000};
|
|
18
|
+
public string Folder, Name;
|
|
19
|
+
public int Bits, Master;
|
|
20
|
+
public bool Protected;
|
|
21
|
+
public byte[] Raw;
|
|
22
|
+
public string ReverbPatch="Not available",ChorusPatch="Not available";
|
|
23
|
+
public readonly TrackState[] Tracks=new TrackState[16];
|
|
24
|
+
public readonly string[] AssignedFiles=new string[16];
|
|
25
|
+
public static R16Project Read(string folder)
|
|
26
|
+
{
|
|
27
|
+
string path=Path.Combine(Path.GetFullPath(folder),"PRJDATA.ZDT");
|
|
28
|
+
byte[] raw=File.ReadAllBytes(path);
|
|
29
|
+
if(raw.Length!=ProjectSize || Text(raw,0,47)!="ZOOM R-16 PROJECT DATA VER0001")
|
|
30
|
+
throw new IOException("This is not a supported R16 VER0001 project. Its files have not been changed.");
|
|
31
|
+
var p=new R16Project { Folder=Path.GetDirectoryName(path),Raw=raw,Name=Text(raw,52,8),Bits=raw[3320]==0?16:24,Master=Int(raw,1188),Protected=raw[48]!=0 };
|
|
32
|
+
if(p.Master<0 || p.Master>127) throw new IOException("Unsupported R16 master level.");
|
|
33
|
+
for(int i=0;i<16;i++) {
|
|
34
|
+
string filename=Text(raw,1192+16*i,12);
|
|
35
|
+
if(filename.Length>0 && !ValidFilename(filename)) throw new IOException("The R16 project contains an unsupported file assignment.");
|
|
36
|
+
p.AssignedFiles[i]=filename;
|
|
37
|
+
int eq=416+48*i;
|
|
38
|
+
var track=new TrackState {
|
|
39
|
+
File=filename.Length==0?"":Path.Combine(p.Folder,"AUDIO",filename),Name=Path.GetFileNameWithoutExtension(filename),
|
|
40
|
+
Fader=Int(raw,96+4*i),Pan=Int(raw,160+4*i)-50,
|
|
41
|
+
Mute=!Bit(raw,84,i),Arm=Bit(raw,80,i),Stereo=Bit(raw,1184,i),Invert=Int(raw,352+4*i)!=0,
|
|
42
|
+
Chorus=Int(raw,224+4*i),Reverb=Int(raw,288+4*i),ChorusOn=Bit(raw,1468,i),ReverbOn=Bit(raw,1472,i),
|
|
43
|
+
HighOn=Int(raw,eq)!=0,HighFreq=Frequency(Int(raw,eq+4),11,17),HighGain=Int(raw,eq+12)-12,
|
|
44
|
+
MidOn=Int(raw,eq+16)!=0,MidFreq=Frequency(Int(raw,eq+20),0,28),MidQ=(Int(raw,eq+24)+1)/10.0,MidGain=Int(raw,eq+28)-12,
|
|
45
|
+
LowOn=Int(raw,eq+32)!=0,LowFreq=Frequency(Int(raw,eq+36),0,17),LowGain=Int(raw,eq+44)-12
|
|
46
|
+
};
|
|
47
|
+
MixerEngine.Validate(track); p.Tracks[i]=track;
|
|
48
|
+
}
|
|
49
|
+
string efx=Path.Combine(p.Folder,"EFXDATA.ZDT");
|
|
50
|
+
if(File.Exists(efx)) {
|
|
51
|
+
byte[] data=File.ReadAllBytes(efx);
|
|
52
|
+
if(data.Length==39032 && Text(data,0,47)=="ZOOM R-16 EFFECT DATA VER0001") {
|
|
53
|
+
p.ChorusPatch=Text(data,0xE8,8)+" (#"+Int(data,0x58)+")";
|
|
54
|
+
p.ReverbPatch=Text(data,0x106,8)+" (#"+Int(data,0x5C)+")";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return p;
|
|
58
|
+
}
|
|
59
|
+
private static int Frequency(int index,int start,int count) { if(index<0 || index>=count) throw new IOException("Unsupported R16 EQ frequency."); return Frequencies[index+start]; }
|
|
60
|
+
public static string Text(byte[] data,int offset,int length) { return Encoding.ASCII.GetString(data,offset,length).Trim('\0',' '); }
|
|
61
|
+
private static int Int(byte[] data,int offset) { return BitConverter.ToInt32(data,offset); }
|
|
62
|
+
private static bool Bit(byte[] data,int offset,int index) { return (BitConverter.ToUInt16(data,offset)&(1<<index))!=0; }
|
|
63
|
+
private static void Put(byte[] data,int offset,int value) { Buffer.BlockCopy(BitConverter.GetBytes(value),0,data,offset,4); }
|
|
64
|
+
private static void SetBit(byte[] data,int offset,int index,bool value) { int mask=BitConverter.ToUInt16(data,offset); mask=value?mask|(1<<index):mask&~(1<<index); data[offset]=(byte)mask; data[offset+1]=(byte)(mask>>8); }
|
|
65
|
+
public static bool ValidFilename(string value) { return Regex.IsMatch(value,@"^[A-Z0-9_\-]{1,8}\.WAV$"); }
|
|
66
|
+
public byte[] Build(MixerSession state,string[] filenames)
|
|
67
|
+
{
|
|
68
|
+
if(state.Tracks==null || state.Tracks.Length!=16 || filenames.Length!=16) throw new IOException("A project must have sixteen tracks.");
|
|
69
|
+
byte[] result=(byte[])Raw.Clone();
|
|
70
|
+
string name=state.Name;
|
|
71
|
+
if(name!=Name) {
|
|
72
|
+
if(string.IsNullOrEmpty(name) || name.Length>8 || name.Any(c=>c<32 || c>126)) throw new IOException("Use 1–8 plain characters for an R16 project name.");
|
|
73
|
+
Buffer.BlockCopy(Encoding.ASCII.GetBytes(name.PadRight(8)),0,result,52,8);
|
|
74
|
+
}
|
|
75
|
+
result[3320]=(byte)(state.Bits==16?0:1);
|
|
76
|
+
if(state.Master<0 || state.Master>127) throw new IOException("Invalid master level.");
|
|
77
|
+
Put(result,1188,state.Master);
|
|
78
|
+
for(int i=0;i<16;i++) {
|
|
79
|
+
TrackState t=state.Tracks[i]; MixerEngine.Validate(t);
|
|
80
|
+
if(filenames[i]!="" && !ValidFilename(filenames[i])) throw new IOException("R16 filenames must be uppercase 8.3 WAV names.");
|
|
81
|
+
if(filenames[i]!=AssignedFiles[i]) {
|
|
82
|
+
Array.Clear(result,1192+16*i,16);
|
|
83
|
+
if(filenames[i].Length>0) Buffer.BlockCopy(Encoding.ASCII.GetBytes(filenames[i].PadRight(12)),0,result,1192+16*i,12);
|
|
84
|
+
}
|
|
85
|
+
SetBit(result,80,i,t.Arm); SetBit(result,84,i,!t.Mute || t.Arm); SetBit(result,1184,i,t.Stereo);
|
|
86
|
+
Put(result,96+4*i,t.Fader); Put(result,160+4*i,t.Pan+50); Put(result,352+4*i,t.Invert?1:0);
|
|
87
|
+
Put(result,224+4*i,t.Chorus); Put(result,288+4*i,t.Reverb); SetBit(result,1468,i,t.ChorusOn); SetBit(result,1472,i,t.ReverbOn);
|
|
88
|
+
int eq=416+48*i;
|
|
89
|
+
PutEq(result,eq,t.HighOn,t.HighFreq,t.HighGain,11,17);
|
|
90
|
+
PutEq(result,eq+16,t.MidOn,t.MidFreq,t.MidGain,0,28);
|
|
91
|
+
Put(result,eq+24,(int)Math.Round(t.MidQ*10)-1);
|
|
92
|
+
PutEq(result,eq+32,t.LowOn,t.LowFreq,t.LowGain,0,17);
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
private static void PutEq(byte[] data,int offset,bool on,int frequency,int gain,int start,int count)
|
|
97
|
+
{
|
|
98
|
+
int index=Array.IndexOf(Frequencies,frequency)-start;
|
|
99
|
+
if(index<0 || index>=count) throw new IOException("This EQ frequency is not supported by the R16.");
|
|
100
|
+
Put(data,offset,on?1:0); Put(data,offset+4,index); Put(data,offset+12,gain+12);
|
|
101
|
+
}
|
|
102
|
+
public static string CopyFolder(string source,string parent,CancellationToken cancel)
|
|
103
|
+
{
|
|
104
|
+
source=Path.GetFullPath(source).TrimEnd(Path.DirectorySeparatorChar);
|
|
105
|
+
parent=Path.GetFullPath(parent).TrimEnd(Path.DirectorySeparatorChar);
|
|
106
|
+
if(parent.Equals(source,StringComparison.OrdinalIgnoreCase) || parent.StartsWith(source+Path.DirectorySeparatorChar,StringComparison.OrdinalIgnoreCase))
|
|
107
|
+
throw new IOException("Choose a destination outside the source project folder.");
|
|
108
|
+
Directory.CreateDirectory(parent);
|
|
109
|
+
string target=null;
|
|
110
|
+
for(int i=0;i<1000;i++) { string candidate=Path.Combine(parent,"PROJ"+i.ToString("000")); if(!Directory.Exists(candidate) && !File.Exists(candidate)) { target=candidate; break; } }
|
|
111
|
+
if(target==null) throw new IOException("There are already 1000 projects in this folder.");
|
|
112
|
+
Directory.CreateDirectory(target);
|
|
113
|
+
CopyTree(source,target,cancel);
|
|
114
|
+
return target;
|
|
115
|
+
}
|
|
116
|
+
private static void CopyTree(string source,string target,CancellationToken cancel)
|
|
117
|
+
{
|
|
118
|
+
foreach(string path in Directory.GetFileSystemEntries(source)) {
|
|
119
|
+
cancel.ThrowIfCancellationRequested();
|
|
120
|
+
var attributes=File.GetAttributes(path);
|
|
121
|
+
if((attributes&FileAttributes.ReparsePoint)!=0) throw new IOException("Linked files are not supported inside an R16 project.");
|
|
122
|
+
string dest=Path.Combine(target,Path.GetFileName(path));
|
|
123
|
+
if((attributes&FileAttributes.Directory)!=0) { Directory.CreateDirectory(dest); CopyTree(path,dest,cancel); }
|
|
124
|
+
else { File.Copy(path,dest,false); File.SetAttributes(dest,File.GetAttributes(dest)&~FileAttributes.ReadOnly); }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
public string SaveCopy(string parent,MixerSession session,Clip[] clips,CancellationToken cancel)
|
|
128
|
+
{
|
|
129
|
+
// The destination is always a newly created project. The source card is never patched.
|
|
130
|
+
string destination=CopyFolder(Folder,parent,cancel);
|
|
131
|
+
var names=(string[])AssignedFiles.Clone();
|
|
132
|
+
Directory.CreateDirectory(Path.Combine(destination,"AUDIO"));
|
|
133
|
+
for(int i=0;i<16;i++) {
|
|
134
|
+
cancel.ThrowIfCancellationRequested();
|
|
135
|
+
if(clips[i]==null) {
|
|
136
|
+
if(string.IsNullOrEmpty(session.Tracks[i].File)) names[i]="";
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
string original=AssignedFiles[i].Length==0?"":Path.Combine(Folder,"AUDIO",AssignedFiles[i]);
|
|
140
|
+
if(string.Equals(Path.GetFullPath(clips[i].Source),original,StringComparison.OrdinalIgnoreCase) && session.Tracks[i].Offset==0) continue;
|
|
141
|
+
Clip clip=clips[i];
|
|
142
|
+
if(session.Tracks[i].Offset==0) {
|
|
143
|
+
double gain=clip.Gain; clip.Gain=0;
|
|
144
|
+
try { names[i]=Path.GetFileName(Audio.Export(clip,Path.Combine(destination,"AUDIO"),session.Bits,false,cancel,null)); }
|
|
145
|
+
finally { clip.Gain=gain; }
|
|
146
|
+
} else {
|
|
147
|
+
string name="TR"+(i+1).ToString("00")+"TAKE";
|
|
148
|
+
string path=Path.Combine(destination,"AUDIO",name+".WAV"); int suffix=0;
|
|
149
|
+
while(File.Exists(path)) path=Path.Combine(destination,"AUDIO",name.Substring(0,6)+(++suffix).ToString("00")+".WAV");
|
|
150
|
+
using(var reader=new WaveFileReader(clip.Cache)) using(var writer=new PcmWriter(path,session.Bits,clip.Channels)) {
|
|
151
|
+
int channels=clip.Channels; float[] block=new float[8192*channels]; long silence=(long)(session.Tracks[i].Offset*Audio.Rate);
|
|
152
|
+
while(silence>0) { cancel.ThrowIfCancellationRequested(); int frames=(int)Math.Min(8192,silence); writer.Write(block,frames*channels,1); silence-=frames; }
|
|
153
|
+
var samples=reader.ToSampleProvider(); int read;
|
|
154
|
+
while((read=samples.Read(block,0,block.Length))>0) { cancel.ThrowIfCancellationRequested(); writer.Write(block,read,1); }
|
|
155
|
+
}
|
|
156
|
+
names[i]=Path.GetFileName(path);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
byte[] updated=Build(session,names);
|
|
160
|
+
string temp=Path.Combine(destination,"PRJDATA.DBUDDY-TEMP"); File.WriteAllBytes(temp,updated);
|
|
161
|
+
File.Replace(temp,Path.Combine(destination,"PRJDATA.ZDT"),null);
|
|
162
|
+
return destination;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Globalization;
|
|
3
|
+
using System.Windows;
|
|
4
|
+
using System.Windows.Media;
|
|
5
|
+
|
|
6
|
+
namespace DrumBuddy
|
|
7
|
+
{
|
|
8
|
+
public enum LampState { Off,Green,Red,Amber }
|
|
9
|
+
public static class PanelFeedback
|
|
10
|
+
{
|
|
11
|
+
public static LampState TrackLamp(TrackState track) { return track.Arm?LampState.Red:track.Solo?LampState.Amber:track.Mute?LampState.Off:LampState.Green; }
|
|
12
|
+
public static bool RecordLamp(bool recording,bool armed,bool pulse) { return recording || (armed && pulse); }
|
|
13
|
+
}
|
|
14
|
+
public sealed class MeterBallistics
|
|
15
|
+
{
|
|
16
|
+
public double Db=-90,HoldDb=-90,ClipSeconds;
|
|
17
|
+
private double holdSeconds;
|
|
18
|
+
public void Update(float peak,double seconds)
|
|
19
|
+
{
|
|
20
|
+
seconds=Math.Max(0,Math.Min(1,seconds));
|
|
21
|
+
double input=peak>0 && !float.IsNaN(peak)?20*Math.Log10(peak):-90;
|
|
22
|
+
Db=Math.Max(input,Db-28*seconds);
|
|
23
|
+
if(input>=HoldDb) { HoldDb=input;holdSeconds=.65; }
|
|
24
|
+
else { holdSeconds-=seconds;if(holdSeconds<=0)HoldDb=Math.Max(Db,HoldDb-22*seconds); }
|
|
25
|
+
if(peak>=.99f)ClipSeconds=1.5;else ClipSeconds=Math.Max(0,ClipSeconds-seconds);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
internal sealed class PanelLamp : FrameworkElement
|
|
29
|
+
{
|
|
30
|
+
private LampState state;
|
|
31
|
+
public LampState State { get { return state; }set { if(state!=value) { state=value;InvalidateVisual(); } } }
|
|
32
|
+
protected override void OnRender(DrawingContext dc)
|
|
33
|
+
{
|
|
34
|
+
double cx=ActualWidth/2,cy=ActualHeight/2,r=Math.Min(cx,cy)*.6;
|
|
35
|
+
Color color=state==LampState.Red?Color.FromRgb(255,46,37):state==LampState.Green?Color.FromRgb(64,246,127):state==LampState.Amber?Color.FromRgb(255,192,38):Color.FromRgb(48,56,51);
|
|
36
|
+
if(state!=LampState.Off) {
|
|
37
|
+
var glow=new RadialGradientBrush(Color.FromArgb(180,color.R,color.G,color.B),Color.FromArgb(0,color.R,color.G,color.B));
|
|
38
|
+
dc.DrawEllipse(glow,null,new Point(cx,cy),r*2,r*2);
|
|
39
|
+
}
|
|
40
|
+
dc.DrawEllipse(new SolidColorBrush(color),new Pen(new SolidColorBrush(Color.FromRgb(40,50,44)),.7),new Point(cx,cy),r,r);
|
|
41
|
+
if(state!=LampState.Off)dc.DrawEllipse(new SolidColorBrush(Color.FromArgb(210,255,255,235)),null,new Point(cx-r*.25,cy-r*.25),r*.24,r*.24);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
internal sealed class PanelMeter : FrameworkElement
|
|
45
|
+
{
|
|
46
|
+
public readonly MeterBallistics Level=new MeterBallistics();
|
|
47
|
+
public string Caption="";
|
|
48
|
+
public bool Compact;
|
|
49
|
+
private static readonly Brush BackgroundBrush=new SolidColorBrush(Color.FromRgb(5,9,8));
|
|
50
|
+
private static readonly Brush Inactive=new SolidColorBrush(Color.FromRgb(29,47,37));
|
|
51
|
+
private static readonly Brush Green=new SolidColorBrush(Color.FromRgb(79,239,115));
|
|
52
|
+
private static readonly Brush Amber=new SolidColorBrush(Color.FromRgb(255,190,42));
|
|
53
|
+
private static readonly Brush Red=new SolidColorBrush(Color.FromRgb(255,59,40));
|
|
54
|
+
public void Update(float peak,double seconds,string caption) { Level.Update(peak,seconds);Caption=caption;InvalidateVisual(); }
|
|
55
|
+
private void Text(DrawingContext dc,string text,double x,double y,double size,Brush color)
|
|
56
|
+
{
|
|
57
|
+
dc.DrawText(new FormattedText(text,CultureInfo.InvariantCulture,FlowDirection.LeftToRight,new Typeface("Consolas"),size,color,VisualTreeHelper.GetDpi(this).PixelsPerDip),new Point(x,y));
|
|
58
|
+
}
|
|
59
|
+
protected override void OnRender(DrawingContext dc)
|
|
60
|
+
{
|
|
61
|
+
dc.DrawRectangle(BackgroundBrush,null,new Rect(0,0,ActualWidth,ActualHeight));
|
|
62
|
+
double x=Compact?5:33,w=Compact?10:12;
|
|
63
|
+
Text(dc,Caption,Compact?1:4,0,9,Brushes.LightGray);
|
|
64
|
+
for(int i=0;i<18;i++) {
|
|
65
|
+
double db=-60+i*(60.0/17),y=18+(17-i)*3.8;
|
|
66
|
+
Brush color=Level.Db>=db-.2?(db>=-3?Red:db>=-12?Amber:Green):Inactive;
|
|
67
|
+
dc.DrawRectangle(color,null,new Rect(x,y,w,2.8));
|
|
68
|
+
}
|
|
69
|
+
double holdY=18+(17-Math.Max(0,Math.Min(17,(Level.HoldDb+60)*17/60)))*3.8;
|
|
70
|
+
if(Level.HoldDb>-60)dc.DrawRectangle(Brushes.Ivory,null,new Rect(x,holdY,w,1));
|
|
71
|
+
if(Level.ClipSeconds>0)dc.DrawRectangle(Red,null,new Rect(x,14,w,3));
|
|
72
|
+
if(!Compact) { Text(dc," 0",4,14,9,Brushes.Gray);Text(dc,"-12",4,28,9,Brushes.Gray);Text(dc,"-24",4,42,9,Brushes.Gray);Text(dc,"-48",4,68,9,Brushes.Gray); }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Text;
|
|
5
|
+
using System.Threading;
|
|
6
|
+
using System.Collections.Generic;
|
|
7
|
+
using NAudio.Wave;
|
|
8
|
+
|
|
9
|
+
namespace DrumBuddy
|
|
10
|
+
{
|
|
11
|
+
public static class PlaybackTests
|
|
12
|
+
{
|
|
13
|
+
private sealed class Samples : ISampleProvider
|
|
14
|
+
{
|
|
15
|
+
private readonly float[] values;
|
|
16
|
+
private int position;
|
|
17
|
+
public WaveFormat WaveFormat { get; private set; }
|
|
18
|
+
public Samples(float[] values, int rate, int channels)
|
|
19
|
+
{ this.values = values; WaveFormat = WaveFormat.CreateIeeeFloatWaveFormat(rate, channels); }
|
|
20
|
+
public int Read(float[] buffer, int offset, int count)
|
|
21
|
+
{ int read = Math.Min(count, values.Length - position); Array.Copy(values, position, buffer, offset, read); position += read; return read; }
|
|
22
|
+
}
|
|
23
|
+
private static float[] ReadAll(ISampleProvider source)
|
|
24
|
+
{
|
|
25
|
+
var result = new List<float>(); var block = new float[258 * source.WaveFormat.Channels]; int read;
|
|
26
|
+
while ((read = source.Read(block, 0, block.Length)) > 0) result.AddRange(block.Take(read));
|
|
27
|
+
return result.ToArray();
|
|
28
|
+
}
|
|
29
|
+
internal static void Offline(Action<bool, string> check)
|
|
30
|
+
{
|
|
31
|
+
foreach (int bits in new[] { 16, 24, 32 }) {
|
|
32
|
+
var input = new Samples(new[] { -2f, -.5f, 0f, .5f, 2f, float.NaN }, 48000, 1);
|
|
33
|
+
var format = new WaveFormat(48000, bits, 1); var pcm = new EndpointWaveProvider(input, format);
|
|
34
|
+
var data = Enumerable.Repeat((byte)0xCC, 6 * bits / 8 + 8).ToArray(); int length = pcm.Read(data, 4, data.Length - 8);
|
|
35
|
+
var values = new List<long>();
|
|
36
|
+
for (int i = 4; i < 4 + length; i += bits / 8) {
|
|
37
|
+
long value = 0; for (int b = 0; b < bits / 8; b++) value |= (long)data[i + b] << (8 * b);
|
|
38
|
+
if ((value & (1L << (bits - 1))) != 0) value -= 1L << bits; values.Add(value);
|
|
39
|
+
}
|
|
40
|
+
long scale = 1L << (bits - 1);
|
|
41
|
+
check(values.SequenceEqual(new[] { -scale, -scale / 2, 0L, scale / 2, scale - 1, 0L }) && data.Take(4).All(b => b == 0xCC) && data.Skip(4 + length).All(b => b == 0xCC),
|
|
42
|
+
"Encode bounded " + bits + "-bit PCM for a Windows endpoint without corrupting buffer offsets");
|
|
43
|
+
}
|
|
44
|
+
var nativeFormat = new WaveFormatExtensible(48000, 32, 6);
|
|
45
|
+
var exact = new EndpointWaveProvider(new Samples(new float[6], 48000, 6), nativeFormat);
|
|
46
|
+
check(object.ReferenceEquals(exact.WaveFormat, nativeFormat), "Keep the exact native endpoint descriptor, including its speaker mask");
|
|
47
|
+
check(new OutputChoice { Compatibility = true }.Key == "compat:default" && new OutputChoice { Compatibility = true, EndpointId = "endpoint" }.Key == "compat:endpoint",
|
|
48
|
+
"Persist compatibility outputs separately from shared Windows and ASIO selections");
|
|
49
|
+
var mono = ReadAll(AudioOutputs.Adapt(new Samples(new[] { .25f, -.75f, 1.5f }, 44100, 1), 44100, 2));
|
|
50
|
+
check(mono.SequenceEqual(new[] { .25f, .25f, -.75f, -.75f, 1f, 1f }), "Duplicate mono into both headphones and bound playback headroom");
|
|
51
|
+
var stereo = ReadAll(AudioOutputs.Adapt(new Samples(new[] { .5f, -.25f, -1f, .5f }, 44100, 2), 44100, 2));
|
|
52
|
+
check(stereo.SequenceEqual(new[] { .5f, -.25f, -1f, .5f }), "Preserve stereo separation at the Windows output boundary");
|
|
53
|
+
var downmix = ReadAll(AudioOutputs.Adapt(new Samples(new[] { .5f, -.25f, -1f, .5f }, 44100, 2), 44100, 1));
|
|
54
|
+
check(downmix.SequenceEqual(new[] { .125f, -.25f }), "Downmix stereo for mono Windows endpoints");
|
|
55
|
+
var surround = AudioOutputs.Adapt(new Samples(new[] { .5f, -.25f, float.NaN, float.PositiveInfinity }, 48000, 2), 48000, 6);
|
|
56
|
+
var wire = new EndpointWaveProvider(surround); var bytes = new byte[64]; int count = wire.Read(bytes, 4, 48);
|
|
57
|
+
check(wire.WaveFormat.Encoding == WaveFormatEncoding.Extensible && wire.WaveFormat.BitsPerSample == 32 && count == 48 &&
|
|
58
|
+
BitConverter.ToSingle(bytes, 4) == .5f && BitConverter.ToSingle(bytes, 8) == -.25f && bytes.Skip(12).Take(40).All(b => b == 0),
|
|
59
|
+
"Supply extensible float audio to multichannel endpoints with silent unused speakers and finite samples");
|
|
60
|
+
foreach (int rate in new[] { 48000, 96000 }) {
|
|
61
|
+
float[] tone = Enumerable.Range(0, 4410).Select(i => (float)(.5 * Math.Sin(2 * Math.PI * 440 * i / 44100))).ToArray();
|
|
62
|
+
var adapted = AudioOutputs.Adapt(new Samples(tone, 44100, 1), rate, 2); var converted = ReadAll(adapted);
|
|
63
|
+
check(Math.Abs(converted.Length / 2.0 / rate - .1) < .001 && converted.All(v => !float.IsNaN(v) && Math.Abs(v) <= 1) && converted.Max() > .4,
|
|
64
|
+
"Resample 44.1 kHz audio to " + rate + " Hz without changing playback duration or losing audio");
|
|
65
|
+
check(converted.Where((v, i) => i % 2 == 0).SequenceEqual(converted.Where((v, i) => i % 2 != 0)), "Keep resampled mono centered at " + rate + " Hz");
|
|
66
|
+
}
|
|
67
|
+
var gesture = new TimelineGesture();
|
|
68
|
+
gesture.Begin(TimelineDrag.Playhead, 200, 100, 20, 80); gesture.Move(400); gesture.End(450);
|
|
69
|
+
check(gesture.Playhead == 450 && gesture.From == 20 && gesture.To == 80 && gesture.Mode == TimelineDrag.None,
|
|
70
|
+
"Drag the playhead continuously and use the final release position without changing the selection");
|
|
71
|
+
check(TimelineGesture.FrameAt(9, 40, new long[0], false) == 0 && TimelineGesture.FrameAt(-10, 40, new long[0], false) == 0,
|
|
72
|
+
"Snap a generous left-edge target and drags past the start to exact zero");
|
|
73
|
+
check(TimelineGesture.FrameAt(9, 40, new long[0], true) > 0, "Alt permits precise placement inside the zero-snap area");
|
|
74
|
+
check(TimelineGesture.FrameAt(77, 40, new[] { Audio.Rate * 2L }, false) == Audio.Rate * 2L &&
|
|
75
|
+
TimelineGesture.FrameAt(77, 40, new[] { Audio.Rate * 2L }, true) != Audio.Rate * 2L,
|
|
76
|
+
"Snap to clip boundaries while allowing Alt to bypass snapping");
|
|
77
|
+
gesture.Begin(TimelineDrag.Selection, 900, 150, 20, 80); gesture.Move(500); gesture.End(300);
|
|
78
|
+
check(gesture.From == 300 && gesture.To == 900 && gesture.Playhead == 150, "Select in either drag direction while retaining the separate playhead");
|
|
79
|
+
gesture.Begin(TimelineDrag.SelectionStart, 300, 150, 300, 900); gesture.Move(500); gesture.End(600);
|
|
80
|
+
check(gesture.From == 600 && gesture.To == 900, "Drag the thin selection-start handle without moving the end");
|
|
81
|
+
gesture.Begin(TimelineDrag.SelectionEnd, 900, 150, 600, 900); gesture.End(1200);
|
|
82
|
+
check(gesture.From == 600 && gesture.To == 1200, "Drag the thin selection-end handle without moving the start");
|
|
83
|
+
gesture.Begin(TimelineDrag.SelectionStart, 600, 150, 600, 1200); gesture.End(1400);
|
|
84
|
+
check(gesture.From == 1200 && gesture.To == 1200, "Prevent a selection-start handle from crossing its end");
|
|
85
|
+
gesture.Begin(TimelineDrag.SelectionEnd, 1200, 150, 600, 1200); gesture.End(400);
|
|
86
|
+
check(gesture.From == 600 && gesture.To == 600, "Prevent a selection-end handle from crossing its start");
|
|
87
|
+
gesture.Begin(TimelineDrag.Playhead, 300, 150, 20, 80); gesture.Move(600); gesture.Cancel();
|
|
88
|
+
check(gesture.Playhead == 150 && gesture.From == 20 && gesture.To == 80 && gesture.Mode == TimelineDrag.None, "Escape restores the values before dragging");
|
|
89
|
+
gesture.Begin(TimelineDrag.Selection, 200, 150, 20, 80); gesture.Move(400); gesture.Finish();
|
|
90
|
+
check(gesture.From == 200 && gesture.To == 400 && gesture.Mode == TimelineDrag.None, "Keep the current selection when mouse capture is lost");
|
|
91
|
+
check(TimelineGesture.RulerHit(105, 20, 100, 200, true) == TimelineDrag.SelectionStart &&
|
|
92
|
+
TimelineGesture.RulerHit(195, 20, 100, 200, true) == TimelineDrag.SelectionEnd &&
|
|
93
|
+
TimelineGesture.RulerHit(105, 5, 100, 200, true) == TimelineDrag.Playhead,
|
|
94
|
+
"Give blue boundary handles broad hit areas while retaining upper-ruler playhead dragging");
|
|
95
|
+
}
|
|
96
|
+
public static int Run(string[] args)
|
|
97
|
+
{
|
|
98
|
+
string root = Path.GetFullPath(args.Length > 0 ? args[0] : Path.Combine(Path.GetTempPath(), "DrumBuddyOutput_" + Guid.NewGuid().ToString("N")));
|
|
99
|
+
Directory.CreateDirectory(root); var log = new List<string>();
|
|
100
|
+
try {
|
|
101
|
+
var choices = new List<OutputChoice> { new OutputChoice { Name = "Windows default" }, new OutputChoice { Name = "Windows default compatibility", Compatibility = true } };
|
|
102
|
+
var named = AudioOutputs.Choices().FirstOrDefault(c => c.Compatibility && c.EndpointId != null);
|
|
103
|
+
if (named != null) choices.Add(named);
|
|
104
|
+
foreach (var choice in choices) foreach (int channels in new[] { 1, 2 }) {
|
|
105
|
+
using (var completed = new ManualResetEvent(false))
|
|
106
|
+
using (var route = AudioOutputs.Open(new Samples(new float[4410 * channels], 44100, channels), choice)) {
|
|
107
|
+
Exception error = null;
|
|
108
|
+
route.Player.PlaybackStopped += delegate(object sender, StoppedEventArgs e) { error = e.Exception; completed.Set(); };
|
|
109
|
+
if (choice.Compatibility && !(route.Player is WaveOutEvent)) throw new Exception("Expected PCM compatibility playback.");
|
|
110
|
+
log.Add("Output: " + route.Description + ", " + route.Counter.WaveFormat.Channels + " channels");
|
|
111
|
+
route.Player.Play();
|
|
112
|
+
if (!completed.WaitOne(5000)) throw new Exception("Timed out while playing silent samples.");
|
|
113
|
+
if (error != null) throw error;
|
|
114
|
+
if (route.Counter.Frames <= 0 || Math.Abs(route.Seconds - .1) > .01) throw new Exception("The output did not consume the expected audio duration.");
|
|
115
|
+
log.Add("PASS Open, play and drain " + channels + "-channel silent source through " + route.Description);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
bool unavailable = false;
|
|
119
|
+
try { using (AudioOutputs.OpenCompatibility(new Samples(new float[4410], 44100, 1), new OutputChoice { Compatibility = true, EndpointId = "missing-endpoint-for-test" })) { } }
|
|
120
|
+
catch (IOException) { unavailable = true; }
|
|
121
|
+
if (!unavailable) throw new Exception("A missing selected endpoint was replaced by another output.");
|
|
122
|
+
log.Add("PASS Refuse to route a missing explicitly selected endpoint to a different device");
|
|
123
|
+
foreach (var input in AudioInputs.Choices().Where(c => c.Driver == null)) {
|
|
124
|
+
int channels = AudioInputs.Channels(input); var rates = AudioInputs.Rates(input, channels).ToArray();
|
|
125
|
+
if (channels < 1 || rates.Length == 0) throw new Exception("Windows microphone capabilities could not be queried: " + input.Name);
|
|
126
|
+
log.Add("PASS Query microphone mapping, " + channels + " channels and rates " + string.Join(", ", rates) + " without recording: " + input.Name);
|
|
127
|
+
}
|
|
128
|
+
log.Add("Silent output probe only: no tone, microphone recording or system-volume change. This does not verify audible sound on another PC.");
|
|
129
|
+
File.WriteAllLines(Path.Combine(root, "output-test-results.txt"), log, Encoding.UTF8); return 0;
|
|
130
|
+
}
|
|
131
|
+
catch (Exception ex) { log.Add("FAIL " + ex); File.WriteAllLines(Path.Combine(root, "output-test-results.txt"), log, Encoding.UTF8); return 1; }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
FLUX 1.5.0
|
|
2
|
+
|
|
3
|
+
INSTALL / UPDATE
|
|
4
|
+
Close the app. Run npm.cmd install -g @pro-gram/flux@latest
|
|
5
|
+
Launch flux.cmd, or the Flux Desktop shortcut.
|
|
6
|
+
Migrating: npm.cmd uninstall -g @pro-gram/drumbuddy before installing Flux.
|
|
7
|
+
Windows 10/11 x64 only in this release.
|
|
8
|
+
Macintosh support is planned.
|
|
9
|
+
|
|
10
|
+
EDITOR + RECORD
|
|
11
|
+
The Editor opens by default. Home contains the separate mini-app sections.
|
|
12
|
+
File contains New, Open session, Save, Save as and Add audio.
|
|
13
|
+
Resize recording options by dragging the divider. Collapse hides its content;
|
|
14
|
+
Show restores it. Its Side/Bottom selector changes the panel position.
|
|
15
|
+
Add/drop audio, select time by dragging, then cut/copy/paste or split.
|
|
16
|
+
Drag the ruler to place the playhead. The left edge snaps to exact zero.
|
|
17
|
+
The thinner blue sticks mark selection start/end. Alt bypasses snapping.
|
|
18
|
+
Range / zoom opens numeric selection and zoom controls.
|
|
19
|
+
Record options opens the sidebar. Select input, channels, rate and format.
|
|
20
|
+
The take is saved at the playhead on the selected track; separate inputs use
|
|
21
|
+
consecutive tracks. Stop saves and inserts it. Source files are preserved.
|
|
22
|
+
Use direct headphone monitoring. Software overdub synchronization is not included.
|
|
23
|
+
|
|
24
|
+
AUDIO DEVICES
|
|
25
|
+
Audio devices at the top right opens Settings > Audio. Save device changes.
|
|
26
|
+
For Realtek/shared-audio errors, select the same device's Compatibility
|
|
27
|
+
(16-bit PCM) output. Test speakers plays a short quiet tone.
|
|
28
|
+
Save diagnostics exports device information and recent playback errors.
|
|
29
|
+
Playback buffer: 40-250 ms; increase it if audio breaks up.
|
|
30
|
+
Windows microphones/Realtek/USB headsets receive 16-bit PCM. Saving that
|
|
31
|
+
as 24-bit PCM or float does not create extra input precision/headroom.
|
|
32
|
+
Use H5studio ASIO for native 32-bit float, with Zoom's official driver.
|
|
33
|
+
USB > Audio Interface > Connect. Use Audio I/F Only for supported rates.
|
|
34
|
+
R16 ASIO capture remains supported. Vendor drivers are linked, not bundled.
|
|
35
|
+
|
|
36
|
+
CONVERTER
|
|
37
|
+
Open View > Converter, or its Home card. Drop audio/video and choose a type.
|
|
38
|
+
General formats: PCM WAV, float WAV, AIFF, FLAC, MP3 and AAC/M4A.
|
|
39
|
+
MP3 -> AIFF and AIFF -> WAV are supported. Gain tools are in Media Player.
|
|
40
|
+
Choose source/44.1/48/96 kHz; MP3 uses at most 48 kHz.
|
|
41
|
+
Conversion options resize, collapse and dock on the side or bottom.
|
|
42
|
+
The Zoom R16 preset writes classic 44.1 kHz PCM with compatible filenames.
|
|
43
|
+
Existing sources and exports are never overwritten.
|
|
44
|
+
|
|
45
|
+
THEMES / SETTINGS
|
|
46
|
+
Top right switches Dark/Light immediately. Settings categories are on the left.
|
|
47
|
+
Keybinds: click Edit; the field automatically highlights and waits for keys.
|
|
48
|
+
Save commits changes. Closing with unsaved changes asks Yes/No to save.
|
|
49
|
+
No discards changes. Duplicate bindings prevent saving until corrected.
|
|
50
|
+
|
|
51
|
+
DEFAULT KEYS
|
|
52
|
+
Space: play/pause or stop active recording. R: start/stop recording.
|
|
53
|
+
Shift+Space: stop. S: split at playhead. K: click-to-split scissors tool.
|
|
54
|
+
Ctrl+X/C/V: cut/copy/paste. Backspace/Delete: remove selection. Ctrl+Z/Y: undo/redo.
|
|
55
|
+
Ctrl+S: save session. Ctrl+Shift+S: save as. Ctrl+O/N: open/new.
|
|
56
|
+
Ctrl+I/E: import/export. Home/End: timeline beginning/end.
|
|
57
|
+
Ctrl+1/2/3/4: Home/Editor/Converter/Player. Ctrl+comma: Settings.
|
|
58
|
+
|
|
59
|
+
MEDIA PLAYER / R16
|
|
60
|
+
Media Player supports PCM/float WAV and other decoded formats.
|
|
61
|
+
Gain / Save amplifies or reduces playback and writes adjusted WAV copies.
|
|
62
|
+
Reset, Set safe level and Prevent clipping are available. Playback volume
|
|
63
|
+
only changes listening level. The gain pane resizes, collapses and docks
|
|
64
|
+
on the side or bottom. Save copy is a compact button; originals are kept.
|
|
65
|
+
R16 Control retains the mixer, MIDI, front panel and project-copy tools.
|
|
66
|
+
Use CARD READER mode for R16 SD-card access; verify project copies on hardware.
|
|
67
|
+
Save .dbedit sessions and keep source files in place. WAV exports: 4 GB limit.
|
|
68
|
+
The original black-haired Drum Buddy logo is preserved in the source archive.
|
|
69
|
+
Basic tools work locally without an account. Premium/billing is not implemented.
|
|
70
|
+
|
|
71
|
+
TEMPO GRID
|
|
72
|
+
Editor BPM/division controls or Settings > Timeline / grid.
|
|
73
|
+
20-400 quarter-note BPM; time signatures; straight/triplet 1/1 to 1/256.
|
|
74
|
+
Grid and clip-edge snapping are independent. Alt bypasses both.
|
|
75
|
+
Tempo changes grid spacing without stretching audio. Save .dbedit to keep it.
|
|
76
|
+
Ctrl+C, Backspace, position playhead, Ctrl+V. Copies survive deletion.
|
|
77
|
+
Zoom to 2560 px/s for fine subdivisions; disable snap for free chops.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Text;
|
|
5
|
+
using System.Threading;
|
|
6
|
+
using System.Collections.Generic;
|
|
7
|
+
using NAudio.Wave;
|
|
8
|
+
|
|
9
|
+
namespace DrumBuddy
|
|
10
|
+
{
|
|
11
|
+
public static class SelfTest
|
|
12
|
+
{
|
|
13
|
+
private static readonly List<string> results = new List<string>();
|
|
14
|
+
private static void Check(bool condition, string message) { if (!condition) throw new Exception(message); results.Add("PASS " + message); }
|
|
15
|
+
public static int Run(string[] args)
|
|
16
|
+
{
|
|
17
|
+
string root = args.Length > 0 ? Path.GetFullPath(args[0]) : Path.Combine(Path.GetTempPath(),"DrumBuddyTest_" + Guid.NewGuid().ToString("N"));
|
|
18
|
+
Directory.CreateDirectory(root);
|
|
19
|
+
try {
|
|
20
|
+
string input = Path.Combine(root,"Practice & Unicode é sample.wav");
|
|
21
|
+
float[] source = new float[44100 * 2 * 2];
|
|
22
|
+
for (int i = 0; i < source.Length / 2; i++) { source[2*i] = (float)(.25 * Math.Sin(2 * Math.PI * 440 * i / 44100)); source[2*i+1] = (float)(.125 * Math.Sin(2 * Math.PI * 880 * i / 44100)); }
|
|
23
|
+
using (var writer = new PcmWriter(input,24,2)) writer.Write(source,source.Length,1);
|
|
24
|
+
Clip original = Audio.Load(input,Path.Combine(root,"cache"),CancellationToken.None,null);
|
|
25
|
+
Check(Math.Abs(original.Duration - 2) < .0001 && original.Channels == 2,"Read stereo WAV with spaces, ampersand and Unicode in path");
|
|
26
|
+
Check(Math.Abs(original.PeakDb - 20 * Math.Log10(.25)) < .002,"Measure full-file sample peak accurately");
|
|
27
|
+
foreach (int bits in new[] { 16,24 }) {
|
|
28
|
+
original.Gain = 6;
|
|
29
|
+
string output = Audio.Export(original,Path.Combine(root,"gain" + bits),bits,false,CancellationToken.None,null);
|
|
30
|
+
using (var reader = new WaveFileReader(output)) {
|
|
31
|
+
Check(reader.WaveFormat.SampleRate == 44100 && reader.WaveFormat.BitsPerSample == bits && reader.WaveFormat.Channels == 2,"Export " + bits + "-bit / 44.1 kHz stereo WAV");
|
|
32
|
+
}
|
|
33
|
+
byte[] header = File.ReadAllBytes(output).Take(44).ToArray();
|
|
34
|
+
Check(BitConverter.ToUInt16(header,20) == 1 && BitConverter.ToInt32(header,16) == 16,"Use classic PCM header for " + bits + "-bit R16 compatibility");
|
|
35
|
+
Clip converted = Audio.Load(output,Path.Combine(root,"cache"),CancellationToken.None,null);
|
|
36
|
+
Check(Math.Abs(converted.PeakDb - original.PeakDb - 6) < .012,"Apply +6 dB gain correctly in " + bits + "-bit export");
|
|
37
|
+
}
|
|
38
|
+
original.Gain = 36;
|
|
39
|
+
string safe = Audio.Export(original,Path.Combine(root,"safe"),24,true,CancellationToken.None,null);
|
|
40
|
+
Clip protectedClip = Audio.Load(safe,Path.Combine(root,"cache"),CancellationToken.None,null);
|
|
41
|
+
Check(Math.Abs(protectedClip.PeakDb + 1) < .002,"Prevent clipping by holding the saved peak at −1 dBFS");
|
|
42
|
+
original.Gain = -6;
|
|
43
|
+
string quieter = Audio.Export(original,Path.Combine(root,"quieter"),24,true,CancellationToken.None,null);
|
|
44
|
+
Clip quietClip = Audio.Load(quieter,Path.Combine(root,"cache"),CancellationToken.None,null);
|
|
45
|
+
Check(Math.Abs(quietClip.PeakDb - original.PeakDb + 6) < .002,"Apply negative gain without normalizing it back up");
|
|
46
|
+
string second = Audio.Export(original,Path.Combine(root,"quieter"),24,true,CancellationToken.None,null);
|
|
47
|
+
Check(second != quieter && File.Exists(quieter),"Keep existing output files and generate a new short name");
|
|
48
|
+
Check(System.Text.RegularExpressions.Regex.IsMatch(Path.GetFileName(second),"^[A-Z0-9_]{1,8}\\.WAV$"),"Generate R16-compatible uppercase 8-character file names");
|
|
49
|
+
Check(Audio.SafeName("AUX!.wav") == "_AUX","Avoid reserved Windows device names");
|
|
50
|
+
foreach (string extension in new[] { "mp3","mp4" }) {
|
|
51
|
+
string media = Path.Combine(root,"actual sample." + extension);
|
|
52
|
+
string argsMedia = extension == "mp3" ? "-i " + Audio.Q(input) + " -c:a libmp3lame -b:a 192k " : "-f lavfi -i color=c=black:s=160x90:r=10 -i " + Audio.Q(input) + " -c:v mpeg4 -c:a aac -shortest ";
|
|
53
|
+
Audio.Run("-hide_banner -v error -nostdin -y " + argsMedia + Audio.Q(media),CancellationToken.None,null,true);
|
|
54
|
+
Clip clip = Audio.Load(media,Path.Combine(root,"cache"),CancellationToken.None,null);
|
|
55
|
+
Check(clip.Ready && Math.Abs(clip.Duration - 2) < .1,"Decode real " + extension.ToUpperInvariant() + (extension == "mp4" ? " video and extract its audio" : " audio"));
|
|
56
|
+
string output = Audio.Export(clip,Path.Combine(root,extension),16,true,CancellationToken.None,null);
|
|
57
|
+
Check(File.Exists(output),"Convert " + extension.ToUpperInvariant() + " to R16 WAV");
|
|
58
|
+
}
|
|
59
|
+
string odd = Path.Combine(root,"odd-mono.wav");
|
|
60
|
+
using (var writer = new PcmWriter(odd,24,1)) writer.Write(new float[] { .25f },1,1);
|
|
61
|
+
byte[] oddBytes = File.ReadAllBytes(odd);
|
|
62
|
+
Check(oddBytes.Length == 48 && BitConverter.ToUInt32(oddBytes,40) == 3 && BitConverter.ToUInt32(oddBytes,4) == 40,"Pad odd-sized 24-bit mono data while keeping correct RIFF lengths");
|
|
63
|
+
Clip mono = Audio.Load(odd,Path.Combine(root,"cache"),CancellationToken.None,null);
|
|
64
|
+
Check(mono.Channels == 1,"Preserve a mono source as mono");
|
|
65
|
+
string invalid = Path.Combine(root,"no audio.mp4"); File.WriteAllText(invalid,"This is not audio");
|
|
66
|
+
bool rejected = false; try { Audio.Load(invalid,Path.Combine(root,"cache"),CancellationToken.None,null); } catch (IOException) { rejected = true; }
|
|
67
|
+
Check(rejected,"Reject corrupt or audio-free inputs cleanly");
|
|
68
|
+
var cancelled = new CancellationTokenSource(); cancelled.Cancel();
|
|
69
|
+
bool stopped = false; try { Audio.Export(original,Path.Combine(root,"cancel"),24,true,cancelled.Token,null); } catch (OperationCanceledException) { stopped = true; }
|
|
70
|
+
Check(stopped && Directory.GetFiles(Path.Combine(root,"cancel")).Length == 0,"Cancel an export and remove incomplete output");
|
|
71
|
+
Check(File.Exists(input) && new FileInfo(input).Length == 44 + source.Length * 3,"Leave original source audio unchanged");
|
|
72
|
+
results.Add("\nAudio checks complete. Hardware-dependent ASIO capture requires a connected R16 and remains a manual acceptance test.");
|
|
73
|
+
File.WriteAllLines(Path.Combine(root,"test-results.txt"),results,Encoding.UTF8); return 0;
|
|
74
|
+
}
|
|
75
|
+
catch (Exception ex) { results.Add("FAIL " + ex); File.WriteAllLines(Path.Combine(root,"test-results.txt"),results,Encoding.UTF8); return 1; }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|