@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.
- package/LICENSE +23 -0
- package/NOTICE.txt +33 -0
- package/README.md +125 -2
- package/bin/flux.js +29 -0
- package/dependencies.json +15 -0
- package/package.json +60 -6
- package/payload/DrumBuddy-Setup.exe +4 -0
- package/scripts/install.js +60 -0
- package/scripts/version.js +12 -0
- package/source/AssetBootstrap.cs +49 -0
- package/source/Audio Decoders.txt +230 -0
- package/source/Audio.cs +453 -0
- package/source/AudioDeviceSettings.cs +126 -0
- package/source/AudioEndpointWatcher.cs +25 -0
- package/source/AudioInputs.cs +50 -0
- package/source/AudioOutputs.cs +307 -0
- package/source/AudioWorkspace.cs +72 -0
- package/source/BrandArtwork.cs +71 -0
- package/source/Build Instructions.txt +65 -0
- package/source/Build.ps1 +135 -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/EditorCompactLayout.cs +36 -0
- package/source/EditorPlaybackTests.cs +87 -0
- package/source/EditorSelectionTests.cs +73 -0
- package/source/EditorToolInteractions.cs +227 -0
- package/source/EditorTools.cs +78 -0
- package/source/EditorToolsTests.cs +93 -0
- package/source/EditorView.cs +798 -0
- package/source/Fetch-Dependencies.ps1 +15 -0
- package/source/FloatRecording.cs +99 -0
- package/source/FloatRecordingTests.cs +170 -0
- package/source/Flux-Logo.png +0 -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 +225 -0
- package/source/InterfaceTests.cs +172 -0
- package/source/LICENSE.txt +23 -0
- package/source/Logo Prompt.txt +16 -0
- package/source/Main.cs +787 -0
- package/source/Main.xaml +289 -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 +141 -0
- package/source/Quick Start.txt +96 -0
- package/source/SelfTest.cs +78 -0
- package/source/SettingsWindow.cs +209 -0
- package/source/Setup.cs +196 -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 +33 -0
- package/source/Timeline.cs +268 -0
- package/source/TimelineGestures.cs +57 -0
- package/source/TimelineZoom.cs +21 -0
- package/source/Uninstall.cs +69 -0
- package/source/WaveformDetail.cs +104 -0
- package/source/WindowsAudioPlayer.cs +137 -0
- package/source/WorkspaceTests.cs +202 -0
- package/source/ZoomTests.cs +95 -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,88 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Text;
|
|
5
|
+
using System.Collections.Generic;
|
|
6
|
+
using System.Threading;
|
|
7
|
+
using System.Xml.Serialization;
|
|
8
|
+
|
|
9
|
+
namespace DrumBuddy
|
|
10
|
+
{
|
|
11
|
+
public static class ControlTests
|
|
12
|
+
{
|
|
13
|
+
private static readonly List<string> log=new List<string>();
|
|
14
|
+
private static void Check(bool yes,string text) { if(!yes)throw new Exception(text);log.Add("PASS "+text); }
|
|
15
|
+
private static float Peak(float[] data) { return data.Max(v=>Math.Abs(v)); }
|
|
16
|
+
private static float[] Read(MixerEngine engine,int frames=4410) { var block=new float[frames*2];lock(engine.Gate)engine.Playing=true;engine.Read(block,0,block.Length);return block; }
|
|
17
|
+
public static int Run(string[] args)
|
|
18
|
+
{
|
|
19
|
+
string root=args.Length>0?Path.GetFullPath(args[0]):Path.Combine(Path.GetTempPath(),"DrumBuddyControlTest_"+Guid.NewGuid().ToString("N"));Directory.CreateDirectory(root);
|
|
20
|
+
try {
|
|
21
|
+
string source=Path.Combine(root,"MONO.WAV");var wave=new float[Audio.Rate];for(int i=0;i<wave.Length;i++)wave[i]=(float)(.2*Math.Sin(2*Math.PI*1000*i/Audio.Rate));
|
|
22
|
+
using(var writer=new PcmWriter(source,24,1))writer.Write(wave,wave.Length,1);
|
|
23
|
+
Clip clip=Audio.Load(source,Path.Combine(root,"cache"),CancellationToken.None,null);
|
|
24
|
+
using(var mixer=new MixerEngine()) {
|
|
25
|
+
mixer.Attach(0,clip);float[] original=Read(mixer);Check(Math.Abs(Peak(original)-.2)<.001,"Play a mono track in both output channels at unity");
|
|
26
|
+
mixer.Tracks[0].Fader=80;mixer.Seek(0);float[] quiet=Read(mixer);Check(Math.Abs(Peak(quiet)/Peak(original)-Math.Pow(10,-5.0/20))<.001,"Use the R16 fader calibration (80 = -5 dB)");
|
|
27
|
+
mixer.Tracks[0].Fader=0;mixer.Seek(0);Check(Peak(Read(mixer))==0,"Fader zero silences playback");
|
|
28
|
+
mixer.Tracks[0].Fader=100;mixer.Tracks[0].Pan=-50;mixer.MasterLeftPeak=0;mixer.MasterRightPeak=0;mixer.Seek(0);float[] left=Read(mixer);Check(left.Where((v,i)=>i%2==1).All(v=>v==0) && Peak(left)>.19,"Pan fully left without leaking into the right channel");
|
|
29
|
+
Check(mixer.MasterLeftPeak>.19 && mixer.MasterRightPeak==0,"Meter left and right master outputs independently");
|
|
30
|
+
mixer.Tracks[0].Pan=0;mixer.Tracks[0].Mute=true;mixer.Seek(0);Check(Peak(Read(mixer))==0,"Mute removes a track from the PC mix");
|
|
31
|
+
mixer.Tracks[0].Mute=false;mixer.Attach(1,clip);mixer.Tracks[1].Solo=true;mixer.Seek(0);Check(Math.Abs(Peak(Read(mixer))-.2)<.001,"Solo isolates the selected track");
|
|
32
|
+
mixer.Tracks[1].Solo=false;mixer.Tracks[1].Invert=true;mixer.Seek(0);Check(Peak(Read(mixer))<.00001,"Phase inversion cancels identical aligned mono tracks");
|
|
33
|
+
mixer.Attach(1,null);mixer.Tracks[0].MidGain=6;mixer.Seek(0);float[] eq=Read(mixer);Check(Peak(eq.Skip(2000).ToArray())>.38 && Peak(eq)<.42,"Apply a real +6 dB parametric EQ at 1 kHz");
|
|
34
|
+
mixer.Tracks[0].MidGain=0;mixer.Tracks[0].Offset=.2;mixer.Seek(0);Check(Peak(Read(mixer))==0,"Keep silence before an overdub's timeline offset");
|
|
35
|
+
mixer.Seek(.2);Check(Peak(Read(mixer))>.19,"Start an offset take at its assigned timeline position");
|
|
36
|
+
mixer.Tracks[0].Offset=0;mixer.Seek(.95);Read(mixer,4410);Check(!mixer.Playing && Math.Abs(mixer.Position-1)<.0001,"Stop exactly at the end of the longest track");
|
|
37
|
+
mixer.Loop=true;mixer.LoopStart=.1;mixer.LoopEnd=.2;mixer.Seek(.19);Read(mixer,4410);Check(mixer.Position>=.1 && mixer.Position<.201,"Wrap an A–B loop within a callback block");
|
|
38
|
+
mixer.Loop=false;mixer.Seek(0);for(int i=1;i<16;i++)mixer.Attach(i,clip);float[] loud=Read(mixer);Check(Peak(loud)<=.989 && mixer.MasterPeak>1,"Prevent summed output overload while retaining an over-level meter reading");
|
|
39
|
+
var snapshot=mixer.Snapshot("TEST",24,"");snapshot.Tracks[0].Pan=25;Check(mixer.Tracks[0].Pan==0,"Snapshot settings without sharing mutable track objects");
|
|
40
|
+
string path=Path.Combine(root,"session.dbuddy");using(var f=File.Create(path))new XmlSerializer(typeof(MixerSession)).Serialize(f,snapshot);MixerSession loaded;using(var f=File.OpenRead(path))loaded=(MixerSession)new XmlSerializer(typeof(MixerSession)).Deserialize(f);Check(loaded.Tracks.Length==16 && loaded.Tracks[0].Pan==25 && loaded.Tracks[0].File==source,"Round-trip the PC session's settings and audio references");
|
|
41
|
+
}
|
|
42
|
+
Check(McuProtocol.Decode(0x7F7FE0).Value==127 && McuProtocol.Decode(0x0000E8).Channel==8,"Decode full-resolution track and master fader messages");
|
|
43
|
+
Check(McuProtocol.Decode(0x7F5F90).Kind=="record" && McuProtocol.Decode(0x005F90)==null,"Handle transport presses and ignore MIDI note releases");
|
|
44
|
+
Check(McuProtocol.Decode(0x7F2F90).Value==8 && McuProtocol.Decode(0x7F0090).Kind=="arm" && McuProtocol.Decode(0x7F1090).Kind=="mute","Map R16 banks, record-arm and mute messages");
|
|
45
|
+
Check(McuProtocol.Decode(0x413CB0).Value==-1 && McuProtocol.Decode(0x013CB0).Value==1,"Decode signed Mackie jog-wheel movement");
|
|
46
|
+
var lampTrack=new TrackState { Arm=true,Mute=true };Check(PanelFeedback.TrackLamp(lampTrack)==LampState.Red,"Give an armed track a red lamp even when its playback is muted");
|
|
47
|
+
lampTrack.Arm=false;Check(PanelFeedback.TrackLamp(lampTrack)==LampState.Off,"Switch the track lamp off when muted and disarmed");
|
|
48
|
+
lampTrack.Mute=false;Check(PanelFeedback.TrackLamp(lampTrack)==LampState.Green,"Show the green playback-enabled track lamp");lampTrack.Solo=true;Check(PanelFeedback.TrackLamp(lampTrack)==LampState.Amber,"Show solo as an amber track lamp");
|
|
49
|
+
Check(PanelFeedback.RecordLamp(true,false,false) && PanelFeedback.RecordLamp(false,true,true) && !PanelFeedback.RecordLamp(false,true,false) && !PanelFeedback.RecordLamp(false,false,true),"Hold REC red during capture and pulse it only when armed at standby");
|
|
50
|
+
var meter=new MeterBallistics();meter.Update(.25f,.075);Check(Math.Abs(meter.Db-20*Math.Log10(.25))<.001 && meter.HoldDb==meter.Db,"Show a measured input peak immediately with peak hold");meter.Update(0,.075);Check(meter.Db>-15 && meter.HoldDb>-13,"Keep meter bars visible between audio callbacks instead of flashing to zero");meter.Update(1,.075);Check(meter.ClipSeconds>1,"Hold the red clip indicator after an overloaded input");for(int i=0;i<30;i++)meter.Update(0,.075);Check(meter.ClipSeconds==0 && meter.Db<-60,"Decay meters and clear the clip indicator after silence");
|
|
51
|
+
var inputSamples=new float[256*8];for(int f=0;f<256;f++)for(int c=0;c<8;c++)inputSamples[f*8+c]=(c+1)*.1f;
|
|
52
|
+
using(var inputMeter=new ControlAudio()) { inputMeter.AccumulateInputPeaks(inputSamples,inputSamples.Length);var peaks=inputMeter.DrainInputPeaks();Check(Enumerable.Range(0,8).All(c=>Math.Abs(peaks[c]-(c+1)*.1f)<.0001),"Meter all eight live inputs without requiring any record-arm state");Check(inputMeter.DrainInputPeaks().All(p=>p==0),"Drain input peak latches without reusing stale readings"); }
|
|
53
|
+
foreach(int quality in new[] {16,24}) { var take=new TakeWriter(Path.Combine(root,"take"+quality),quality,new[] {1,6});take.AddSamples(inputSamples,256);take.Finish().GetAwaiter().GetResult();Check(take.Error==null && take.Paths.Count==2 && take.Frames==256,"Save only the armed physical inputs in a "+quality+"-bit capture");Clip inputTwo=Audio.Load(take.Paths[1],Path.Combine(root,"cache"),CancellationToken.None,null),inputSeven=Audio.Load(take.Paths[6],Path.Combine(root,"cache"),CancellationToken.None,null);Check(Math.Abs(inputTwo.Peak-.2)<.0001 && Math.Abs(inputSeven.Peak-.7)<.0001,"Keep channel 2 and channel 7 audio separate in "+quality+"-bit takes"); }
|
|
54
|
+
Check(new Uri(DriverSetup.OfficialUrl).Host=="zoomcorp.com","Link driver setup directly to Zoom's official support site");
|
|
55
|
+
string midiPath=Path.Combine(root,"performance.mid");var midiEvents=new[] { new MidiMessage { Seconds=0,Message=0x643C90 },new MidiMessage { Seconds=.25,Message=0x7F40B0 },new MidiMessage { Seconds=.5,Message=0x003C80 },new MidiMessage { Seconds=.75,Message=0x0005C1 },new MidiMessage { Seconds=1,Message=0x4010E0 } };
|
|
56
|
+
MidiFile.Save(midiPath,midiEvents,1.25);var midiRead=MidiFile.Load(midiPath);Check(midiRead.Length==5 && midiRead.Select(e=>e.Message).SequenceEqual(midiEvents.Select(e=>e.Message)),"Round-trip MIDI notes, velocity, controllers, program changes and pitch bend");Check(midiRead.Zip(midiEvents,(a,b)=>Math.Abs(a.Seconds-b.Seconds)).Max()<.0011,"Preserve MIDI event timing within one file tick");
|
|
57
|
+
using(var performance=new MidiPerformance()) { performance.Begin();performance.Accept(0x643C90);performance.Accept(0x7F40B0);string saved=performance.Finish(Path.Combine(root,"midi-take"));var captured=MidiFile.Load(saved);Check(captured.Length==3 && (captured[0].Message&255)==144 && (captured[2].Message&255)==128,"Capture MIDI performance and close held notes when the take stops");Check(performance.NoteCount==1 && !performance.Recording,"Monitor MIDI note activity separately from Mackie control messages"); }
|
|
58
|
+
string runningPath=Path.Combine(root,"running.mid");byte[] runningBytes={77,84,104,100,0,0,0,6,0,0,0,1,1,224,77,84,114,107,0,0,0,12,0,144,60,100,0x83,0x60,60,0,0,255,47,0};File.WriteAllBytes(runningPath,runningBytes);var running=MidiFile.Load(runningPath);Check(running.Length==2 && Math.Abs(running[1].Seconds-.5)<.0001,"Read standard MIDI running status and quarter-note timing");File.WriteAllBytes(runningPath,runningBytes.Take(runningBytes.Length-1).ToArray());bool badMidi=false;try{MidiFile.Load(runningPath);}catch(IOException){badMidi=true;}Check(badMidi,"Reject a malformed MIDI track instead of reading beyond its bounds");
|
|
59
|
+
if(args.Length>1 && Directory.Exists(args[1])) {
|
|
60
|
+
int checkedFiles=0;
|
|
61
|
+
foreach(string fixture in Directory.GetFiles(args[1],"*.ZDT").Where(p=>!Path.GetFileName(p).StartsWith("EFX",StringComparison.OrdinalIgnoreCase))) {
|
|
62
|
+
string folder=Path.Combine(root,"fixture_"+Path.GetFileNameWithoutExtension(fixture));Directory.CreateDirectory(folder);File.Copy(fixture,Path.Combine(folder,"PRJDATA.ZDT"),true);
|
|
63
|
+
R16Project p=R16Project.Read(folder);var state=new MixerSession { Name=p.Name,Bits=p.Bits,Master=p.Master,Tracks=p.Tracks.Select(t=>t.Copy()).ToArray() };
|
|
64
|
+
byte[] unchanged=p.Build(state,p.AssignedFiles);Check(unchanged.SequenceEqual(File.ReadAllBytes(fixture)),"Preserve every byte on unchanged native project: "+Path.GetFileName(fixture));
|
|
65
|
+
state.Tracks[0].Pan=state.Tracks[0].Pan==23?24:23;state.Tracks[0].Fader=79;
|
|
66
|
+
byte[] changed=p.Build(state,p.AssignedFiles);Check(changed.Where((v,i)=>i<96 || (i>=100 && i<160) || i>=164).SequenceEqual(unchanged.Where((v,i)=>i<96 || (i>=100 && i<160) || i>=164)),"Change only identified pan/fader fields: "+Path.GetFileName(fixture));
|
|
67
|
+
File.WriteAllBytes(Path.Combine(folder,"PRJDATA.ZDT"),changed);R16Project reload=R16Project.Read(folder);Check(reload.Tracks[0].Pan==state.Tracks[0].Pan && reload.Tracks[0].Fader==79,"Read edited native values: "+Path.GetFileName(fixture));checkedFiles++;
|
|
68
|
+
}
|
|
69
|
+
Check(checkedFiles>=8,"Validate native project round-trips against multiple independent hardware fixtures");
|
|
70
|
+
string original=Path.Combine(root,"native-original");Directory.CreateDirectory(original);Directory.CreateDirectory(Path.Combine(original,"AUDIO"));File.Copy(Path.Combine(args[1],"PRJDATA.ZDT"),Path.Combine(original,"PRJDATA.ZDT"),true);File.Copy(Path.Combine(args[1],"EFXDATA1.ZDT"),Path.Combine(original,"EFXDATA.ZDT"),true);File.WriteAllBytes(Path.Combine(original,"unknown.bin"),new byte[]{9,7,2,4});
|
|
71
|
+
R16Project project=R16Project.Read(original);Check(project.ReverbPatch!="Not available" && project.ChorusPatch!="Not available","Read the saved reverb and chorus preset identities");
|
|
72
|
+
var session=new MixerSession { Name=project.Name,Bits=project.Bits,Master=project.Master,Tracks=project.Tracks.Select(t=>t.Copy()).ToArray() };session.Tracks[0].Pan=-17;
|
|
73
|
+
string saved=project.SaveCopy(Path.Combine(root,"copies"),session,new Clip[16],CancellationToken.None);
|
|
74
|
+
Check(R16Project.Read(saved).Tracks[0].Pan==-17 && R16Project.Read(original).Tracks[0].Pan!=-17,"Save supported edits to a new project while keeping the source untouched");
|
|
75
|
+
Check(File.ReadAllBytes(Path.Combine(saved,"EFXDATA.ZDT")).SequenceEqual(File.ReadAllBytes(Path.Combine(original,"EFXDATA.ZDT"))) && File.ReadAllBytes(Path.Combine(saved,"unknown.bin")).SequenceEqual(new byte[]{9,7,2,4}),"Copy effects and unknown state byte-for-byte");
|
|
76
|
+
string second=project.SaveCopy(Path.Combine(root,"copies"),session,new Clip[16],CancellationToken.None);Check(saved!=second && Directory.Exists(saved),"Never overwrite an existing project copy");
|
|
77
|
+
bool blocked=false;try { R16Project.CopyFolder(original,Path.Combine(original,"nested"),CancellationToken.None); }catch(IOException){blocked=true;}Check(blocked,"Reject recursive copies inside the source project");
|
|
78
|
+
session.Tracks[0].File=source;var newClips=new Clip[16];newClips[0]=clip;string withAudio=project.SaveCopy(Path.Combine(root,"copies"),session,newClips,CancellationToken.None);R16Project assigned=R16Project.Read(withAudio);Check(File.Exists(assigned.Tracks[0].File) && R16Project.ValidFilename(assigned.AssignedFiles[0]),"Export newly assigned audio as R16 PCM WAV with a valid native assignment");
|
|
79
|
+
bool refused=false;File.WriteAllBytes(Path.Combine(root,"invalid.ZDT"),new byte[3332]);string bad=Path.Combine(root,"bad");Directory.CreateDirectory(bad);File.Copy(Path.Combine(root,"invalid.ZDT"),Path.Combine(bad,"PRJDATA.ZDT"),true);try{R16Project.Read(bad);}catch(IOException){refused=true;}Check(refused,"Reject unsupported native headers instead of guessing at their state");
|
|
80
|
+
}
|
|
81
|
+
using(var panel=new ControlMode(root,root))panel.RenderFeedbackCheck(Path.Combine(root,"panel-feedback-check.png"),clip);
|
|
82
|
+
Check(true,"Render the actual front panel with an armed red lamp and all input/master meters responding to generated samples");
|
|
83
|
+
log.Add("Hardware MIDI exchange, ASIO transport and R16 reload of an edited project require a connected-device acceptance test.");
|
|
84
|
+
File.WriteAllLines(Path.Combine(root,"control-test-results.txt"),log,Encoding.UTF8);return 0;
|
|
85
|
+
}catch(Exception ex) { log.Add("FAIL "+ex);File.WriteAllLines(Path.Combine(root,"control-test-results.txt"),log,Encoding.UTF8);return 1; }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Globalization;
|
|
4
|
+
using System.Text.RegularExpressions;
|
|
5
|
+
using System.Threading;
|
|
6
|
+
|
|
7
|
+
namespace DrumBuddy
|
|
8
|
+
{
|
|
9
|
+
public sealed class ConversionOptions
|
|
10
|
+
{
|
|
11
|
+
public string Format = "WAV PCM";
|
|
12
|
+
public int Bits = 24, SampleRate;
|
|
13
|
+
public bool R16, Protect = true;
|
|
14
|
+
public string OutputStem;
|
|
15
|
+
}
|
|
16
|
+
public static class ConversionFormats
|
|
17
|
+
{
|
|
18
|
+
public static string Export(Clip clip, string folder, ConversionOptions options, CancellationToken cancel, Action<double> progress)
|
|
19
|
+
{
|
|
20
|
+
if (options.R16) return Audio.Export(clip, folder, options.Bits, options.Protect, cancel, progress);
|
|
21
|
+
string extension, container, codec;
|
|
22
|
+
switch (options.Format) {
|
|
23
|
+
case "WAV PCM": extension = ".wav"; container = "wav"; codec = "-c:a pcm_s" + options.Bits + "le -rf64 auto"; break;
|
|
24
|
+
case "WAV float": extension = ".wav"; container = "wav"; codec = "-c:a pcm_f32le -rf64 auto"; break;
|
|
25
|
+
case "FLAC": extension = ".flac"; container = "flac"; codec = options.Bits == 16 ? "-c:a flac -sample_fmt s16" : "-c:a flac -sample_fmt s32 -bits_per_raw_sample 24"; break;
|
|
26
|
+
case "MP3": extension = ".mp3"; container = "mp3"; codec = "-c:a libmp3lame -b:a 320k"; break;
|
|
27
|
+
case "AAC": extension = ".m4a"; container = "ipod"; codec = "-c:a aac -b:a 256k"; break;
|
|
28
|
+
case "AIFF": extension = ".aiff"; container = "aiff"; codec = "-c:a pcm_s" + options.Bits + "be"; break;
|
|
29
|
+
default: throw new IOException("Choose a supported conversion format.");
|
|
30
|
+
}
|
|
31
|
+
if (options.Bits != 16 && options.Bits != 24) throw new IOException("Choose 16-bit or 24-bit PCM depth.");
|
|
32
|
+
int rate = options.SampleRate == 0 ? clip.SourceRate : options.SampleRate;
|
|
33
|
+
if (rate <= 0) rate = Audio.Rate;
|
|
34
|
+
if (options.Format == "MP3" && rate > 48000) rate = 48000;
|
|
35
|
+
if (rate < 8000 || rate > 384000) throw new IOException("Choose a supported sample rate.");
|
|
36
|
+
Directory.CreateDirectory(folder);
|
|
37
|
+
string temporary = Path.Combine(folder, ".fluxtempo-" + Guid.NewGuid().ToString("N") + ".partial");
|
|
38
|
+
try {
|
|
39
|
+
string volume = Math.Pow(10, clip.AppliedGain(options.Protect) / 20).ToString("R", CultureInfo.InvariantCulture);
|
|
40
|
+
Audio.Run("-hide_banner -v error -nostdin -y -protocol_whitelist file,pipe -i " + Audio.Q(clip.Source) +
|
|
41
|
+
" -map 0:a:0 -vn -sn -dn -map_metadata -1 -af volume=" + volume + " -ar " + rate + " -ac " + clip.Channels +
|
|
42
|
+
" " + codec + " -progress pipe:1 -f " + container + " " + Audio.Q(temporary), cancel, delegate(string line) {
|
|
43
|
+
double us; if (progress != null && clip.Duration > 0 && line.StartsWith("out_time_us=") && double.TryParse(line.Substring(12), NumberStyles.Float, CultureInfo.InvariantCulture, out us)) progress(Math.Min(1, us / 1000000 / clip.Duration));
|
|
44
|
+
}, true);
|
|
45
|
+
cancel.ThrowIfCancellationRequested();
|
|
46
|
+
string stem = Regex.Replace(string.IsNullOrWhiteSpace(options.OutputStem) ? Path.GetFileNameWithoutExtension(clip.Name) : options.OutputStem, @"[\x00-\x1F<>:""/\\|?*]", "_").Trim().TrimEnd('.');
|
|
47
|
+
if (string.IsNullOrEmpty(stem)) stem = "Audio"; if (stem.Length > 120) stem = stem.Substring(0, 120);
|
|
48
|
+
for (int i = 1; i < 100000; i++) {
|
|
49
|
+
string destination = Path.Combine(folder, stem + (i == 1 ? "" : " (" + i + ")") + extension);
|
|
50
|
+
if (File.Exists(destination)) continue;
|
|
51
|
+
try { File.Move(temporary, destination); if (progress != null) progress(1); return destination; }
|
|
52
|
+
catch (IOException) { if (!File.Exists(destination)) throw; }
|
|
53
|
+
}
|
|
54
|
+
throw new IOException("Choose an output folder with fewer duplicate names.");
|
|
55
|
+
}
|
|
56
|
+
finally { Audio.TryDelete(temporary); }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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 ConversionTests
|
|
12
|
+
{
|
|
13
|
+
private static bool IsPcm(WaveFormat format)
|
|
14
|
+
{
|
|
15
|
+
if (format.Encoding == WaveFormatEncoding.Pcm) return true;
|
|
16
|
+
var extra = format as WaveFormatExtraData;
|
|
17
|
+
return format.Encoding == WaveFormatEncoding.Extensible && extra != null && extra.ExtraData.Length >= 22 &&
|
|
18
|
+
new Guid(extra.ExtraData.Skip(6).Take(16).ToArray()) == new Guid("00000001-0000-0010-8000-00aa00389b71");
|
|
19
|
+
}
|
|
20
|
+
public static int Run(string[] args)
|
|
21
|
+
{
|
|
22
|
+
string root = Path.GetFullPath(args.Length > 0 ? args[0] : Path.Combine(Path.GetTempPath(), "FluxConversion_" + Guid.NewGuid().ToString("N")));
|
|
23
|
+
Directory.CreateDirectory(root); var log = new List<string>();
|
|
24
|
+
Action<bool, string> check = delegate(bool value, string message) { if (!value) throw new Exception(message); log.Add("PASS " + message); };
|
|
25
|
+
try {
|
|
26
|
+
string source = Path.Combine(root, "Native take.wav"); var samples = new float[96000 / 2];
|
|
27
|
+
for (int i = 0; i < samples.Length; i++) samples[i] = (float)(1.8 * Math.Sin(2 * Math.PI * 440 * i / 96000));
|
|
28
|
+
using (var writer = new FloatWaveWriter(source, 1, 96000)) writer.Write(samples, samples.Length, 1);
|
|
29
|
+
byte[] master = File.ReadAllBytes(source); var clip = Audio.Load(source, Path.Combine(root, "cache"), CancellationToken.None, null);
|
|
30
|
+
check(clip.SourceRate == 96000, "Keep the source sample rate separate from the editor working rate");
|
|
31
|
+
var options = new ConversionOptions { Format = "WAV float", Protect = false };
|
|
32
|
+
string floatCopy = ConversionFormats.Export(clip, Path.Combine(root, "exports"), options, CancellationToken.None, null);
|
|
33
|
+
using (var reader = new WaveFileReader(floatCopy)) check(reader.WaveFormat.SampleRate == 96000 && reader.WaveFormat.BitsPerSample == 32 && reader.Length == samples.Length * 4L,
|
|
34
|
+
"Convert from the original source without reducing its native rate or float duration");
|
|
35
|
+
check(Audio.Load(floatCopy, Path.Combine(root, "cache"), CancellationToken.None, null).Peak > 1.7, "Retain over-0-dBFS headroom in an unprotected float conversion");
|
|
36
|
+
string mp3 = null, aiff = null;
|
|
37
|
+
foreach (string format in new[] { "WAV PCM", "FLAC", "MP3", "AAC", "AIFF" }) {
|
|
38
|
+
var target = new ConversionOptions { Format = format, SampleRate = 48000, Bits = 24, Protect = true };
|
|
39
|
+
string path = ConversionFormats.Export(clip, Path.Combine(root, "exports"), target, CancellationToken.None, null);
|
|
40
|
+
var decoded = Audio.Load(path, Path.Combine(root, "cache"), CancellationToken.None, null);
|
|
41
|
+
check(decoded.Ready && decoded.SourceRate == 48000 && Math.Abs(decoded.Duration - .5) < .08,
|
|
42
|
+
"Convert to playable " + format + " at 48 kHz with the expected duration");
|
|
43
|
+
if (format == "WAV PCM" || format == "FLAC" || format == "AIFF") check(decoded.Peak <= .90, "Apply safe listening/export gain to " + format);
|
|
44
|
+
if (format == "MP3") mp3 = path;
|
|
45
|
+
if (format == "AIFF") aiff = path;
|
|
46
|
+
}
|
|
47
|
+
var mp3Clip = Audio.Load(mp3, Path.Combine(root, "cache"), CancellationToken.None, null);
|
|
48
|
+
string mp3Aiff = ConversionFormats.Export(mp3Clip, Path.Combine(root, "round-trips"), new ConversionOptions { Format = "AIFF", Bits = 16 }, CancellationToken.None, null);
|
|
49
|
+
byte[] form = File.ReadAllBytes(mp3Aiff);
|
|
50
|
+
check(Encoding.ASCII.GetString(form, 0, 4) == "FORM" && Encoding.ASCII.GetString(form, 8, 4) == "AIFF", "Convert an MP3 into a real AIFF container");
|
|
51
|
+
var aiffClip = Audio.Load(aiff, Path.Combine(root, "cache"), CancellationToken.None, null);
|
|
52
|
+
string aiffWav = ConversionFormats.Export(aiffClip, Path.Combine(root, "round-trips"), new ConversionOptions { Format = "WAV PCM", Bits = 24 }, CancellationToken.None, null);
|
|
53
|
+
using (var reader = new WaveFileReader(aiffWav)) check(reader.WaveFormat.SampleRate == 48000 && reader.WaveFormat.BitsPerSample == 24 && IsPcm(reader.WaveFormat),
|
|
54
|
+
"Convert AIFF to a 24-bit PCM WAV with the original sample rate");
|
|
55
|
+
string second = ConversionFormats.Export(clip, Path.Combine(root, "exports"), options, CancellationToken.None, null);
|
|
56
|
+
check(second != floatCopy && File.Exists(floatCopy), "Give duplicate output names a new filename instead of overwriting an existing export");
|
|
57
|
+
var cancelled = new CancellationTokenSource(); cancelled.Cancel(); bool stopped = false;
|
|
58
|
+
try { ConversionFormats.Export(clip, Path.Combine(root, "cancel"), options, cancelled.Token, null); } catch (OperationCanceledException) { stopped = true; }
|
|
59
|
+
check(stopped && !Directory.GetFiles(Path.Combine(root, "cancel"), "*.partial").Any(), "Cancel general conversion and remove its incomplete output");
|
|
60
|
+
check(master.SequenceEqual(File.ReadAllBytes(source)), "Preserve original audio through all general-format conversions");
|
|
61
|
+
File.WriteAllLines(Path.Combine(root, "conversion-test-results.txt"), log, Encoding.UTF8); return 0;
|
|
62
|
+
}
|
|
63
|
+
catch (Exception ex) { log.Add("FAIL " + ex); File.WriteAllLines(Path.Combine(root, "conversion-test-results.txt"), log, Encoding.UTF8); return 1; }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Windows;
|
|
3
|
+
using System.Windows.Controls;
|
|
4
|
+
using System.Windows.Controls.Primitives;
|
|
5
|
+
using System.Windows.Input;
|
|
6
|
+
|
|
7
|
+
namespace DrumBuddy
|
|
8
|
+
{
|
|
9
|
+
public sealed class DockablePane : Grid
|
|
10
|
+
{
|
|
11
|
+
private readonly FrameworkElement main;
|
|
12
|
+
private readonly Border frame;
|
|
13
|
+
private readonly ScrollViewer content;
|
|
14
|
+
private readonly GridSplitter divider;
|
|
15
|
+
private readonly Button restore;
|
|
16
|
+
private readonly ComboBox location;
|
|
17
|
+
private double sideSize = 310, bottomSize = 210;
|
|
18
|
+
private bool visible, expanded = true, updating;
|
|
19
|
+
public bool IsPaneVisible { get { return visible; } }
|
|
20
|
+
public bool IsExpanded { get { return visible && expanded; } }
|
|
21
|
+
public bool OnBottom { get { return location.SelectedIndex == 1; } }
|
|
22
|
+
public FrameworkElement MainContent { get { return main; } }
|
|
23
|
+
public double MinimumMainHeight = 130;
|
|
24
|
+
|
|
25
|
+
public DockablePane(FrameworkElement main, FrameworkElement accessory, string title, bool bottom = false, bool show = true, double bottomHeight = 210)
|
|
26
|
+
{
|
|
27
|
+
this.main = main; visible = show; bottomSize = bottomHeight; Children.Add(main);
|
|
28
|
+
var panel = new Grid(); panel.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); panel.RowDefinitions.Add(new RowDefinition());
|
|
29
|
+
var heading = new Grid { Margin = new Thickness(9, 5, 7, 5) };
|
|
30
|
+
heading.ColumnDefinitions.Add(new ColumnDefinition()); heading.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); heading.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
|
31
|
+
var label = WorkspaceUi.Text(title, 13); label.FontWeight = FontWeights.SemiBold; label.TextWrapping = TextWrapping.NoWrap; label.TextTrimming = TextTrimming.CharacterEllipsis; heading.Children.Add(label);
|
|
32
|
+
location = new ComboBox { ItemsSource = new[] { "Side", "Bottom" }, Width = 84, MinHeight = 28, FontSize = 12, Margin = new Thickness(5, 0, 5, 0), ToolTip = "Display on side or display on bottom" };
|
|
33
|
+
SetColumn(location, 1); heading.Children.Add(location);
|
|
34
|
+
var collapse = WorkspaceUi.Button("Collapse", delegate { SetExpanded(false); }); collapse.Margin = new Thickness(0); collapse.MinHeight = 28; collapse.Padding = new Thickness(7, 2, 7, 2); SetColumn(collapse, 2); heading.Children.Add(collapse);
|
|
35
|
+
panel.Children.Add(heading);
|
|
36
|
+
content = new ScrollViewer { Content = accessory, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled }; SetRow(content, 1); panel.Children.Add(content);
|
|
37
|
+
frame = new Border { Background = WorkspaceUi.Color("#FFFFFF"), BorderBrush = WorkspaceUi.Color("#C3CED0"), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(5), Child = panel }; Children.Add(frame);
|
|
38
|
+
divider = new GridSplitter { Background = WorkspaceUi.Color("#C3CED0"), ResizeBehavior = GridResizeBehavior.PreviousAndNext, ShowsPreview = true, ToolTip = "Drag to resize this panel", Focusable = true };
|
|
39
|
+
divider.DragCompleted += delegate { RememberSize(); }; Children.Add(divider);
|
|
40
|
+
restore = WorkspaceUi.Button("Show " + title, delegate { SetExpanded(true); }); restore.Margin = new Thickness(0); restore.ToolTip = "Expand " + title; Children.Add(restore);
|
|
41
|
+
location.SelectedIndex = bottom ? 1 : 0;
|
|
42
|
+
location.SelectionChanged += delegate { Configure(); };
|
|
43
|
+
SizeChanged += delegate { LimitSize(); };
|
|
44
|
+
Configure();
|
|
45
|
+
}
|
|
46
|
+
public void SetContent(FrameworkElement accessory) { content.Content = accessory; }
|
|
47
|
+
public void ShowPane(bool value) { visible = value; if (value) expanded = true; Configure(); }
|
|
48
|
+
public void SetExpanded(bool value) { expanded = value; Configure(); }
|
|
49
|
+
public void SetBottom(bool value) { location.SelectedIndex = value ? 1 : 0; }
|
|
50
|
+
private void RememberSize()
|
|
51
|
+
{
|
|
52
|
+
if (!visible || !expanded || updating) return;
|
|
53
|
+
if (RowDefinitions.Count == 3) bottomSize = RowDefinitions[2].ActualHeight > 0 ? RowDefinitions[2].ActualHeight : bottomSize;
|
|
54
|
+
if (ColumnDefinitions.Count == 3) sideSize = ColumnDefinitions[2].ActualWidth > 0 ? ColumnDefinitions[2].ActualWidth : sideSize;
|
|
55
|
+
}
|
|
56
|
+
private void Configure()
|
|
57
|
+
{
|
|
58
|
+
updating = true;
|
|
59
|
+
try {
|
|
60
|
+
ColumnDefinitions.Clear(); RowDefinitions.Clear();
|
|
61
|
+
foreach (UIElement item in Children) { SetColumn(item, 0); SetRow(item, 0); }
|
|
62
|
+
if (OnBottom) {
|
|
63
|
+
RowDefinitions.Add(new RowDefinition()); RowDefinitions.Add(new RowDefinition { Height = new GridLength(visible && expanded ? 6 : 0) });
|
|
64
|
+
RowDefinitions.Add(new RowDefinition { Height = new GridLength(!visible ? 0 : expanded ? bottomSize : 32) });
|
|
65
|
+
SetRow(frame, 2); SetRow(restore, 2); SetRow(divider, 1);
|
|
66
|
+
divider.ResizeDirection = GridResizeDirection.Rows; divider.Cursor = Cursors.SizeNS; divider.HorizontalAlignment = HorizontalAlignment.Stretch; divider.VerticalAlignment = VerticalAlignment.Stretch;
|
|
67
|
+
restore.Content = "Show panel"; restore.HorizontalAlignment = HorizontalAlignment.Left; restore.VerticalAlignment = VerticalAlignment.Center;
|
|
68
|
+
} else {
|
|
69
|
+
ColumnDefinitions.Add(new ColumnDefinition()); ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(visible && expanded ? 6 : 0) });
|
|
70
|
+
ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(!visible ? 0 : expanded ? sideSize : 32) });
|
|
71
|
+
SetColumn(frame, 2); SetColumn(restore, 2); SetColumn(divider, 1);
|
|
72
|
+
divider.ResizeDirection = GridResizeDirection.Columns; divider.Cursor = Cursors.SizeWE; divider.HorizontalAlignment = HorizontalAlignment.Stretch; divider.VerticalAlignment = VerticalAlignment.Stretch;
|
|
73
|
+
restore.Content = "‹"; restore.HorizontalAlignment = HorizontalAlignment.Stretch; restore.VerticalAlignment = VerticalAlignment.Top;
|
|
74
|
+
}
|
|
75
|
+
frame.Visibility = divider.Visibility = visible && expanded ? Visibility.Visible : Visibility.Collapsed;
|
|
76
|
+
restore.Visibility = visible && !expanded ? Visibility.Visible : Visibility.Collapsed;
|
|
77
|
+
LimitSize();
|
|
78
|
+
}
|
|
79
|
+
finally { updating = false; }
|
|
80
|
+
}
|
|
81
|
+
private void LimitSize()
|
|
82
|
+
{
|
|
83
|
+
if (!visible || !expanded) return;
|
|
84
|
+
if (OnBottom && RowDefinitions.Count == 3) {
|
|
85
|
+
double maximum = Math.Max(54, ActualHeight - MinimumMainHeight - 6); if (ActualHeight <= 0) maximum = 400;
|
|
86
|
+
RowDefinitions[2].MinHeight = Math.Min(90, maximum); RowDefinitions[2].MaxHeight = maximum;
|
|
87
|
+
RowDefinitions[2].Height = new GridLength(Math.Min(bottomSize, maximum));
|
|
88
|
+
} else if (ColumnDefinitions.Count == 3) {
|
|
89
|
+
double maximum = Math.Max(230, ActualWidth - 300); if (ActualWidth <= 0) maximum = 500;
|
|
90
|
+
ColumnDefinitions[2].MinWidth = Math.Min(230, maximum); ColumnDefinitions[2].MaxWidth = maximum;
|
|
91
|
+
ColumnDefinitions[2].Width = new GridLength(Math.Min(sideSize, maximum));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Diagnostics;
|
|
3
|
+
using System.Windows;
|
|
4
|
+
using System.Windows.Controls;
|
|
5
|
+
using System.Windows.Media;
|
|
6
|
+
|
|
7
|
+
namespace DrumBuddy
|
|
8
|
+
{
|
|
9
|
+
public static class DriverSetup
|
|
10
|
+
{
|
|
11
|
+
public const string OfficialUrl="https://zoomcorp.com/en/us/digital-mixer-multi-track-recorders/multi-track-recorders/r16/r16-support/";
|
|
12
|
+
public const string H5studioUrl="https://zoomcorp.com/en/us/handheld-recorders/handheld-recorders/h5studio/h5studio-support/";
|
|
13
|
+
public static void ShowGuide(Window owner)
|
|
14
|
+
{
|
|
15
|
+
var guide=new Window { Title="Zoom driver setup",Owner=owner,WindowStartupLocation=WindowStartupLocation.CenterOwner,Width=580,Height=Math.Min(590,SystemParameters.WorkArea.Height-60),ResizeMode=ResizeMode.NoResize,Background=Brushes.White,FontFamily=new FontFamily("Segoe UI"),FontSize=17 };
|
|
16
|
+
var content=new StackPanel { Margin=new Thickness(26) };guide.Content=new ScrollViewer { Content=content,VerticalScrollBarVisibility=ScrollBarVisibility.Auto };
|
|
17
|
+
content.Children.Add(new TextBlock { Text="Install the official Zoom driver",FontSize=23,FontWeight=FontWeights.SemiBold,TextWrapping=TextWrapping.Wrap,Margin=new Thickness(0,0,0,16) });
|
|
18
|
+
content.Children.Add(new TextBlock { Text="1. Choose your recorder's official download page below.\n2. Download its Windows 10/11 64-bit ASIO driver.\n3. Disconnect the recorder, extract the ZIP and run the installer.\n4. Finish Zoom's installer; restart if it asks.\n5. Reconnect USB and choose AUDIO INTERFACE on the recorder.\n6. Reopen Flux, or click Refresh in Record drums.",TextWrapping=TextWrapping.Wrap,LineHeight=26 });
|
|
19
|
+
foreach (string model in new[] { "H5studio", "R16 / R24" }) {
|
|
20
|
+
string url=model=="H5studio"?H5studioUrl:OfficialUrl;
|
|
21
|
+
var open=new Button { Content="Open Zoom " + model + " driver downloads",MinHeight=46,Margin=new Thickness(0,12,0,0),FontSize=17,Padding=new Thickness(10) };
|
|
22
|
+
open.Click+=delegate { try { Process.Start(new ProcessStartInfo(url) { UseShellExecute=true }); }catch(Exception ex) { MessageBox.Show(guide,ex.Message,"Could not open the browser"); } };content.Children.Add(open);
|
|
23
|
+
}
|
|
24
|
+
content.Children.Add(new TextBlock { Text="For H5studio float recording, select its ASIO driver in Record drums and choose 32-bit float. Audio I/F Only lets the computer set the sample rate.",TextWrapping=TextWrapping.Wrap,FontSize=15,Margin=new Thickness(0,14,0,12) });
|
|
25
|
+
var close=new Button { Content="Close",MinHeight=36,IsCancel=true };close.Click+=delegate { guide.Close(); };content.Children.Add(close);guide.ShowDialog();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Linq;
|
|
3
|
+
using System.Windows;
|
|
4
|
+
using System.Windows.Controls;
|
|
5
|
+
|
|
6
|
+
namespace DrumBuddy
|
|
7
|
+
{
|
|
8
|
+
public sealed partial class EditorView
|
|
9
|
+
{
|
|
10
|
+
private void ArrangeCompactChrome(Panel top, Panel editing, Panel range, Panel viewControls, Panel zoomTools, Panel bottom, Panel transport, Panel exports)
|
|
11
|
+
{
|
|
12
|
+
top.Children.Remove(title); top.Children.Remove(cancel); top.Visibility = Visibility.Collapsed;
|
|
13
|
+
title.FontSize = 14; title.MaxWidth = 180; title.TextWrapping = TextWrapping.NoWrap; title.TextTrimming = TextTrimming.CharacterEllipsis; title.Margin = new Thickness(0, 0, 12, 4);
|
|
14
|
+
editing.Children.Insert(0, title); editing.Children.Insert(1, cancel);
|
|
15
|
+
editing.Children.Remove(separate); separate.Content = "Separate import tracks"; range.Children.Add(separate);
|
|
16
|
+
compactOptionsButton.Content = "Export settings"; scope.Width = 132;
|
|
17
|
+
foreach (var control in editing.Children.OfType<Control>()) { control.FontSize = 12; control.MinHeight = 26; control.Padding = new Thickness(6, 2, 6, 2); control.Margin = new Thickness(0, 0, 4, 4); }
|
|
18
|
+
viewControls.Children.Remove(zoomTools); viewControls.Children.Remove(selectionText); viewControls.Visibility = Visibility.Collapsed;
|
|
19
|
+
zoomTools.Margin = new Thickness(10, 0, 0, 0); gridControls.Children.Add(zoomTools);
|
|
20
|
+
foreach (var control in gridControls.Children.OfType<Control>()) { control.FontSize = 12; control.MinHeight = 26; }
|
|
21
|
+
tempoBox.Width = 50; gridDivision.Width = 72; rulerMode.Width = 104;
|
|
22
|
+
foreach (var button in gridControls.Children.OfType<Button>()) { button.Content = "Grid…"; button.MinHeight = 26; button.Padding = new Thickness(6, 2, 6, 2); }
|
|
23
|
+
foreach (var button in zoomTools.Children.OfType<Button>()) { button.MinHeight = 24; button.Padding = new Thickness(4, 1, 4, 1); button.Margin = new Thickness(0, 0, 4, 4); }
|
|
24
|
+
fitSelection.Content = "Sel"; zoomSpan.Width = 62;
|
|
25
|
+
bottom.Margin = new Thickness(0, 5, 0, 0);
|
|
26
|
+
play.MinWidth = 52;
|
|
27
|
+
foreach (var button in transport.Children.OfType<Button>()) { button.FontSize = 12; button.MinHeight = 26; button.Padding = new Thickness(6, 2, 6, 2); button.Margin = new Thickness(0, 0, 4, 4); }
|
|
28
|
+
positionText.FontSize = 12; positionText.Margin = new Thickness(3, 0, 9, 4); volume.Width = 80; volume.Margin = new Thickness(0, 0, 8, 4);
|
|
29
|
+
foreach (var text in transport.Children.OfType<TextBlock>().Where(t => t != positionText)) { text.FontSize = 12; if (text.Text.StartsWith("Listen")) text.Text = "Volume "; }
|
|
30
|
+
bottom.Children.Remove(outputPicker); outputPicker.UseCompactLayout(); transport.Children.Add(outputPicker);
|
|
31
|
+
selectionText.FontSize = 11; selectionText.MaxWidth = 220; selectionText.Margin = new Thickness(5, 0, 0, 4); transport.Children.Add(selectionText);
|
|
32
|
+
foreach (var control in exports.Children.OfType<Control>()) { control.FontSize = 12; control.MinHeight = 26; control.Margin = new Thickness(0, 0, 8, 3); }
|
|
33
|
+
exportFormat.Width = 150;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Text;
|
|
5
|
+
using System.Threading;
|
|
6
|
+
using System.Threading.Tasks;
|
|
7
|
+
using System.Collections.Generic;
|
|
8
|
+
using System.Diagnostics;
|
|
9
|
+
using System.Windows;
|
|
10
|
+
using System.Windows.Threading;
|
|
11
|
+
using NAudio.Wave;
|
|
12
|
+
|
|
13
|
+
namespace DrumBuddy
|
|
14
|
+
{
|
|
15
|
+
public static class EditorPlaybackTests
|
|
16
|
+
{
|
|
17
|
+
private static readonly List<string> log = new List<string>();
|
|
18
|
+
private static void Check(bool value, string message) { if (!value) throw new Exception(message); log.Add("PASS " + message); }
|
|
19
|
+
private static void Pump(Func<bool> ready, int milliseconds)
|
|
20
|
+
{
|
|
21
|
+
var frame = new DispatcherFrame(); var watch = Stopwatch.StartNew();
|
|
22
|
+
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(10) };
|
|
23
|
+
timer.Tick += delegate { if (ready() || watch.ElapsedMilliseconds > milliseconds) { timer.Stop(); frame.Continue = false; } };
|
|
24
|
+
timer.Start(); Dispatcher.PushFrame(frame);
|
|
25
|
+
}
|
|
26
|
+
private static void RunUi(Func<Task> action)
|
|
27
|
+
{
|
|
28
|
+
var previous = SynchronizationContext.Current;
|
|
29
|
+
SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext(Dispatcher.CurrentDispatcher));
|
|
30
|
+
try { var task = action(); Pump(() => task.IsCompleted, 20000); if (!task.IsCompleted) throw new TimeoutException("Editor action did not finish."); task.GetAwaiter().GetResult(); }
|
|
31
|
+
finally { SynchronizationContext.SetSynchronizationContext(previous); }
|
|
32
|
+
}
|
|
33
|
+
private sealed class Silence : ISampleProvider
|
|
34
|
+
{
|
|
35
|
+
private int remaining = Audio.Rate;
|
|
36
|
+
public WaveFormat WaveFormat { get { return WaveFormat.CreateIeeeFloatWaveFormat(Audio.Rate, 1); } }
|
|
37
|
+
public int Read(float[] data, int offset, int count) { int read = Math.Min(count, remaining); Array.Clear(data, offset, read); remaining -= read; return read; }
|
|
38
|
+
}
|
|
39
|
+
public static int Run(string[] args)
|
|
40
|
+
{
|
|
41
|
+
string root = Path.GetFullPath(args[0]); Directory.CreateDirectory(root); log.Clear();
|
|
42
|
+
var previous = InterfacePreferences.Current; string oldKey = AudioOutputs.PreferredKey, oldLog = AudioOutputs.TestErrorLog;
|
|
43
|
+
AudioOutputs.TestErrorLog = Path.Combine(root, "playback-errors.txt");
|
|
44
|
+
try {
|
|
45
|
+
InterfacePreferences.Current = InterfacePreferences.Defaults(); AudioOutputs.PreferredKey = "default";
|
|
46
|
+
using (var done = new ManualResetEvent(false)) using (var player = new WindowsAudioPlayer(new Silence(), null, false, 80)) {
|
|
47
|
+
Exception stopped = null; player.PlaybackStopped += delegate(object sender, StoppedEventArgs e) { stopped = e.Exception; done.Set(); };
|
|
48
|
+
player.Play(); Thread.Sleep(80); player.Pause(); Thread.Sleep(70); long a = player.GetPosition(); Thread.Sleep(70); long b = player.GetPosition();
|
|
49
|
+
Check(player.PlaybackState == PlaybackState.Paused && a > 0 && a == b, "Pause a native polling stream without advancing its audible position");
|
|
50
|
+
player.Play(); Check(done.WaitOne(5000) && stopped == null && player.Counter.Frames > Audio.Rate / 2, "Resume and drain the native shared polling stream");
|
|
51
|
+
}
|
|
52
|
+
foreach (string kind in new[] { "float-mono-44100", "float-stereo-48000", "float-eight-channel-96000", "pcm16", "pcm24" }) {
|
|
53
|
+
bool pcm = kind.StartsWith("pcm"); int channels = kind.Contains("eight") ? 8 : kind.Contains("stereo") || kind == "pcm24" ? 2 : 1;
|
|
54
|
+
int rate = kind.Contains("96000") ? 96000 : kind.Contains("48000") ? 48000 : Audio.Rate;
|
|
55
|
+
string file = Path.Combine(root, kind + ".wav"); var samples = new float[rate / 5 * channels];
|
|
56
|
+
for (int i = 0; i < samples.Length; i++) samples[i] = (float)((pcm ? .6 : 1.5) * Math.Sin(i / channels * .08));
|
|
57
|
+
if (pcm) { using (var writer = new PcmWriter(file, kind == "pcm16" ? 16 : 24, channels)) writer.Write(samples, samples.Length, 1); }
|
|
58
|
+
else { using (var writer = new FloatWaveWriter(file, channels, rate)) writer.Write(samples, samples.Length, 1); }
|
|
59
|
+
if (channels == 8) {
|
|
60
|
+
string extensible = Path.Combine(root, "float-extensible.wav");
|
|
61
|
+
Audio.Run("-hide_banner -v error -nostdin -i " + Audio.Q(file) + " -c:a pcm_f32le " + Audio.Q(extensible), CancellationToken.None, null, true); file = extensible;
|
|
62
|
+
using (var wave = new WaveFileReader(file)) Check(wave.WaveFormat.Encoding == WaveFormatEncoding.Extensible, "Exercise a real multichannel WAVEFORMATEXTENSIBLE float file");
|
|
63
|
+
}
|
|
64
|
+
byte[] original = File.ReadAllBytes(file); var messages = new List<string>(); var owner = new Window(); ThemePalette.Install(owner);
|
|
65
|
+
using (var editor = new EditorView(owner, Path.Combine(root, "cache-" + kind), text => messages.Add(text), delegate { })) {
|
|
66
|
+
owner.Content = editor; editor.ListenVolumeForTest(0);
|
|
67
|
+
RunUi(() => editor.AddFiles(new[] { file }));
|
|
68
|
+
Check(editor.SessionForTest.Regions.Count == 1 && editor.SessionForTest.End > 0, "Import " + kind + " into the actual editor");
|
|
69
|
+
long end = editor.SessionForTest.End;
|
|
70
|
+
RunUi(() => editor.ExecuteAction("play"));
|
|
71
|
+
Pump(() => editor.PlayheadFrame == end || messages.Any(t => t.StartsWith("Could not play") || t.StartsWith("Editor playback stopped")), 5000);
|
|
72
|
+
Check(editor.PlayheadFrame == end && !messages.Any(t => t.StartsWith("Could not play") || t.StartsWith("Editor playback stopped")), "Play and finish imported " + kind + " through the actual editor transport at muted listening volume");
|
|
73
|
+
}
|
|
74
|
+
owner.Content = null; owner.Close();
|
|
75
|
+
Check(original.SequenceEqual(File.ReadAllBytes(file)), "Preserve the original " + kind + " file through import and playback");
|
|
76
|
+
}
|
|
77
|
+
var picker = new AudioOutputPicker(delegate { }, delegate { });
|
|
78
|
+
AudioOutputs.PreferredKey = "windows:missing-output-for-test"; picker.RefreshForTest();
|
|
79
|
+
Check(picker.SelectionForTest == "default" && AudioOutputs.PreferredKey == "default", "Keep the displayed default and actual saved routing selection consistent after a device disappears");
|
|
80
|
+
log.Add("Local Windows output used Cirrus Logic. Muted listening preserves system volume. Affected Realtek-device acceptance remains required.");
|
|
81
|
+
File.WriteAllLines(Path.Combine(root, "editor-playback-test-results.txt"), log, Encoding.UTF8); return 0;
|
|
82
|
+
}
|
|
83
|
+
catch (Exception ex) { log.Add("FAIL " + ex); File.WriteAllLines(Path.Combine(root, "editor-playback-test-results.txt"), log, Encoding.UTF8); return 1; }
|
|
84
|
+
finally { InterfacePreferences.Current = previous; AudioOutputs.PreferredKey = oldKey; AudioOutputs.TestErrorLog = oldLog; }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Text;
|
|
5
|
+
using System.Collections.Generic;
|
|
6
|
+
using System.Windows;
|
|
7
|
+
using System.Windows.Input;
|
|
8
|
+
using System.Windows.Media;
|
|
9
|
+
using System.Windows.Media.Imaging;
|
|
10
|
+
|
|
11
|
+
namespace DrumBuddy
|
|
12
|
+
{
|
|
13
|
+
public static class EditorSelectionTests
|
|
14
|
+
{
|
|
15
|
+
private static readonly List<string> log = new List<string>();
|
|
16
|
+
private static void Check(bool value, string text) { if (!value) throw new Exception(text); log.Add("PASS " + text); }
|
|
17
|
+
private static long Frame(double seconds) { return (long)Math.Round(seconds * Audio.Rate); }
|
|
18
|
+
private static void Picture(FrameworkElement view, string file)
|
|
19
|
+
{
|
|
20
|
+
view.Measure(new Size(1100, 650)); view.Arrange(new Rect(0, 0, 1100, 650)); view.UpdateLayout();
|
|
21
|
+
var bitmap = new RenderTargetBitmap(1100, 650, 96, 96, PixelFormats.Pbgra32); bitmap.Render(view);
|
|
22
|
+
var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(bitmap)); using (var output = File.Create(file)) encoder.Save(output);
|
|
23
|
+
}
|
|
24
|
+
public static int Run(string[] args)
|
|
25
|
+
{
|
|
26
|
+
string root = Path.GetFullPath(args[0]); Directory.CreateDirectory(root); log.Clear(); var previous = InterfacePreferences.Current;
|
|
27
|
+
try {
|
|
28
|
+
InterfacePreferences.Current = InterfacePreferences.Defaults(); InterfacePreferences.Current.SnapToGrid = InterfacePreferences.Current.SnapToEdges = false;
|
|
29
|
+
Check(InterfacePreferences.Resolve(Key.V, ModifierKeys.None) == "selectTool" && InterfacePreferences.Resolve(Key.V, ModifierKeys.Control) == "paste" && InterfacePreferences.Resolve(Key.S, ModifierKeys.None) == "split", "Keep V selection, S split and Ctrl+V paste as separate actions");
|
|
30
|
+
Check(InterfacePreferences.Resolve(Key.OemPlus, ModifierKeys.Control | ModifierKeys.Shift) == "zoomInPlus" && InterfacePreferences.Resolve(Key.Add, ModifierKeys.Control) == "zoomInKeypad" && InterfacePreferences.Resolve(Key.Subtract, ModifierKeys.Control) == "zoomOutKeypad", "Recognize literal Ctrl+plus and numeric-keypad zoom shortcuts");
|
|
31
|
+
string file = Path.Combine(root, "Original float.wav"); var samples = new float[Audio.Rate * 4]; for (int i = 0; i < samples.Length; i++) samples[i] = (float)(.7 * Math.Sin(i * .03));
|
|
32
|
+
using (var writer = new FloatWaveWriter(file, 1, Audio.Rate)) writer.Write(samples, samples.Length, 1);
|
|
33
|
+
byte[] original = File.ReadAllBytes(file);
|
|
34
|
+
var clip = new Clip { Source = file, Cache = file, Name = "Original float.wav", Channels = 1, Duration = 4, Peak = .7, Waveform = Enumerable.Repeat(.7f, 900).ToArray(), Ready = true };
|
|
35
|
+
var state = new EditorSession { Name = "Selection tools" };
|
|
36
|
+
var first = new TimelineRegion { Id = "first", Source = file, Name = "Selected audio", Track = 0, Start = Frame(1), Length = Frame(2) };
|
|
37
|
+
var overlap = new TimelineRegion { Id = "overlap", Source = file, Name = "Overlapping unselected audio", Track = 0, Start = Frame(1.5), SourceStart = Frame(.5), Length = Frame(1) };
|
|
38
|
+
var other = new TimelineRegion { Id = "other", Source = file, Name = "Second selected track", Track = 1, Start = Frame(2), Length = Frame(1.5) };
|
|
39
|
+
state.Regions.AddRange(new[] { first, overlap, other });
|
|
40
|
+
var assets = new Dictionary<string, Clip> { { file, clip } }; var owner = new Window(); ThemePalette.Apply(true); ThemePalette.Install(owner);
|
|
41
|
+
using (var editor = new EditorView(owner, Path.Combine(root, "cache"), delegate { }, delegate { })) {
|
|
42
|
+
owner.Content = editor; editor.SetForTest(state, assets); Picture(editor, Path.Combine(root, "selection-overview.png")); editor.ZoomForTest(320);
|
|
43
|
+
editor.PointerDownForTest(Frame(1.25), 0, false); editor.PointerUpForTest(Frame(1.25));
|
|
44
|
+
Check(editor.SelectedClipsForTest == 1 && editor.HighlightedForTest == 1, "Click one clip and highlight only that clip despite overlapping audio");
|
|
45
|
+
editor.ExecuteAction("copy").GetAwaiter().GetResult(); editor.ExecuteAction("delete").GetAwaiter().GetResult();
|
|
46
|
+
Check(editor.SessionForTest.Regions.Count == 2 && editor.SessionForTest.Regions.Any(r => r.Id == "overlap"), "Delete the selected clip without deleting overlapping unselected audio");
|
|
47
|
+
editor.PlayheadForTest(Frame(5)); editor.ExecuteAction("paste").GetAwaiter().GetResult();
|
|
48
|
+
Check(editor.SessionForTest.Regions.Any(r => r.Start == Frame(5) && r.Length == Frame(2) && r.SourceStart == 0) && editor.SelectedClipsForTest == 1, "Paste the copied clip at the playhead and select the new item");
|
|
49
|
+
editor.ExecuteAction("undo").GetAwaiter().GetResult(); Check(editor.SessionForTest.Regions.Count == 2, "Undo the selected-clip paste independently");
|
|
50
|
+
editor.SetForTest(state, assets); editor.PointerDownForTest(Frame(1.25), 0, false); editor.PointerUpForTest(Frame(1.25)); editor.PointerDownForTest(Frame(3), 1, true);
|
|
51
|
+
Check(editor.SelectedClipsForTest == 2 && editor.HighlightedForTest == 2, "Shift-click selects clips across tracks with distinct purple highlights");
|
|
52
|
+
editor.ExecuteAction("zoomSelection").GetAwaiter().GetResult(); Picture(editor, Path.Combine(root, "selected-clips-dark.png")); ThemePalette.Apply(false); Picture(editor, Path.Combine(root, "selected-clips-light.png")); ThemePalette.Apply(true);
|
|
53
|
+
editor.ExecuteAction("copy").GetAwaiter().GetResult(); editor.ExecuteAction("delete").GetAwaiter().GetResult();
|
|
54
|
+
Check(editor.SessionForTest.Regions.Count == 1 && editor.SessionForTest.Regions[0].Id == "overlap", "Multi-clip deletion preserves unselected regions between selected items");
|
|
55
|
+
editor.PlayheadForTest(Frame(5)); editor.ExecuteAction("paste").GetAwaiter().GetResult();
|
|
56
|
+
Check(editor.SessionForTest.Regions.Any(r => r.Track == 0 && r.Start == Frame(5)) && editor.SessionForTest.Regions.Any(r => r.Track == 1 && r.Start == Frame(6)) && editor.SessionForTest.Regions.Count == 3, "Copy/paste selected clips preserves their relative timing and track layout");
|
|
57
|
+
editor.SetForTest(state, assets); editor.PointerDownForTest(Frame(1.75), 0, false); editor.PointerMoveForTest(Frame(2.25));
|
|
58
|
+
Check(editor.SelectedClipsForTest == 0 && editor.HighlightedForTest == 2, "Dragging within audio automatically changes to a precise time range with live highlights");
|
|
59
|
+
editor.PointerUpForTest(Frame(2.25)); editor.ExecuteAction("delete").GetAwaiter().GetResult();
|
|
60
|
+
Check(editor.SessionForTest.Regions.Where(r => r.Track == 0).All(r => r.End <= Frame(1.75) || r.Start >= Frame(2.25)) && editor.SessionForTest.Regions.Any(r => r.Id == "other"), "Backspace on a dragged range removes that time only from the selected track");
|
|
61
|
+
editor.SetForTest(state, assets); editor.PointerDownForTest(Frame(1.25), 0, false); editor.PointerUpForTest(Frame(1.25)); editor.ExecuteAction("split").GetAwaiter().GetResult();
|
|
62
|
+
Check(editor.SessionForTest.Regions.Count == 4 && editor.SessionForTest.Regions.Single(r => r.Id == "overlap").Length == Frame(1), "S splits the selected clip at the playhead without splitting an unselected overlap");
|
|
63
|
+
editor.ExecuteAction("scissors").GetAwaiter().GetResult(); editor.ExecuteAction("selectTool").GetAwaiter().GetResult(); Check(editor.SelectCursorForTest, "V immediately restores the normal selection pointer");
|
|
64
|
+
double zoom = editor.ZoomPixelsForTest; editor.ExecuteAction("zoomInPlus").GetAwaiter().GetResult(); editor.ExecuteAction("zoomOutKeypad").GetAwaiter().GetResult(); Check(editor.ZoomPixelsForTest == zoom, "Execute plus/minus zoom actions without modifying the audio");
|
|
65
|
+
}
|
|
66
|
+
owner.Content = null; owner.Close(); Check(original.SequenceEqual(File.ReadAllBytes(file)), "Preserve the original audio through clip selection, range editing and split operations");
|
|
67
|
+
File.WriteAllLines(Path.Combine(root, "selection-test-results.txt"), log, Encoding.UTF8); return 0;
|
|
68
|
+
}
|
|
69
|
+
catch (Exception ex) { log.Add("FAIL " + ex); File.WriteAllLines(Path.Combine(root, "selection-test-results.txt"), log, Encoding.UTF8); return 1; }
|
|
70
|
+
finally { InterfacePreferences.Current = previous; ThemePalette.Apply(previous.Theme == "Dark"); }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|