@pro-gram/flux 1.6.1 → 1.6.4
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/README.md +18 -3
- package/bin/flux.js +1 -1
- package/dependencies.json +15 -15
- package/package.json +61 -60
- package/payload/DrumBuddy-Setup.exe +2 -2
- package/source/AssetBootstrap.cs +1 -1
- package/source/Audio Decoders.txt +1 -1
- package/source/Audio.cs +27 -5
- package/source/Build Instructions.txt +9 -1
- package/source/Build.ps1 +7 -1
- package/source/EditorCompactLayout.cs +1 -1
- package/source/EditorMixer.cs +267 -0
- package/source/EditorMixerTests.cs +159 -0
- package/source/EditorPlaybackTests.cs +22 -0
- package/source/EditorRecording.cs +106 -0
- package/source/EditorView.cs +49 -25
- package/source/InterfaceSettings.cs +11 -4
- package/source/Main.cs +110 -53
- package/source/Main.xaml +2 -1
- package/source/MediaPlayerView.cs +1 -1
- package/source/Quick Start.txt +25 -4
- package/source/RecordingSession.cs +59 -0
- package/source/RecordingTests.cs +143 -0
- package/source/Setup.cs +6 -5
- package/source/ShellStyles.xaml +11 -5
- package/source/TempoGrid.cs +15 -1
- package/source/TempoGridTests.cs +40 -0
- package/source/Third Party Notices.txt +1 -1
- package/source/Timeline.cs +39 -13
- package/source/TimelineMixer.cs +44 -0
- package/source/references/R16 Format Research.txt +0 -16
package/source/Setup.cs
CHANGED
|
@@ -14,7 +14,7 @@ using System.Windows.Markup;
|
|
|
14
14
|
using Microsoft.Win32;
|
|
15
15
|
|
|
16
16
|
[assembly: AssemblyTitle("Flux Setup")]
|
|
17
|
-
[assembly: AssemblyVersion("1.6.
|
|
17
|
+
[assembly: AssemblyVersion("1.6.4.0")]
|
|
18
18
|
|
|
19
19
|
namespace DrumBuddySetup
|
|
20
20
|
{
|
|
@@ -101,7 +101,7 @@ namespace DrumBuddySetup
|
|
|
101
101
|
Directory.CreateDirectory(Path.GetDirectoryName(destination));
|
|
102
102
|
File.Copy(source,destination,true);
|
|
103
103
|
}
|
|
104
|
-
File.WriteAllText(Path.Combine(InstallPath,".drumbuddy-installed"),"Flux 1.6.
|
|
104
|
+
File.WriteAllText(Path.Combine(InstallPath,".drumbuddy-installed"),"Flux 1.6.4\n" + InstallPath,Encoding.UTF8);
|
|
105
105
|
string[] files = Directory.GetFiles(stage,"*",SearchOption.AllDirectories).Select(f => f.Substring(stage.Length + 1)).Concat(new[] { ".drumbuddy-installed","installed-files.txt" }).ToArray();
|
|
106
106
|
File.WriteAllLines(Path.Combine(InstallPath,"installed-files.txt"),files,Encoding.UTF8);
|
|
107
107
|
string exe = Path.Combine(InstallPath,"DrumBuddy.exe");
|
|
@@ -112,7 +112,7 @@ namespace DrumBuddySetup
|
|
|
112
112
|
RemoveLegacyShortcut(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu),"Programs","Drum Buddy.lnk"),exe);
|
|
113
113
|
RemoveLegacyShortcut(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),"Drum Buddy.lnk"),exe);
|
|
114
114
|
using (var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(RegistryKey)) {
|
|
115
|
-
key.SetValue("DisplayName","Flux"); key.SetValue("DisplayVersion","1.6.
|
|
115
|
+
key.SetValue("DisplayName","Flux"); key.SetValue("DisplayVersion","1.6.4"); key.SetValue("Publisher","Flux");
|
|
116
116
|
key.SetValue("InstallLocation",InstallPath); key.SetValue("DisplayIcon",BrandIcon);
|
|
117
117
|
key.SetValue("UninstallString","\"" + Path.Combine(InstallPath,"Uninstall.exe") + "\"");
|
|
118
118
|
key.SetValue("NoModify",1); key.SetValue("NoRepair",1);
|
|
@@ -137,17 +137,18 @@ namespace DrumBuddySetup
|
|
|
137
137
|
foreach (var entry in zip.Entries) {
|
|
138
138
|
if (string.IsNullOrEmpty(entry.Name)) continue;
|
|
139
139
|
string relative = entry.FullName.Replace('/',Path.DirectorySeparatorChar);
|
|
140
|
+
string manifestName = entry.FullName.Replace('\\','/');
|
|
140
141
|
string target = Path.GetFullPath(Path.Combine(root,relative));
|
|
141
142
|
if (!target.StartsWith(root,StringComparison.OrdinalIgnoreCase) || relative.Contains(":")) throw new IOException("Invalid installer file path.");
|
|
142
143
|
string expected;
|
|
143
|
-
if (!hashes.TryGetValue(
|
|
144
|
+
if (!hashes.TryGetValue(manifestName,out expected)) throw new IOException("An installer file is not in the integrity manifest.");
|
|
144
145
|
Directory.CreateDirectory(Path.GetDirectoryName(target));
|
|
145
146
|
using (var input = entry.Open()) using (var output = new FileStream(target,FileMode.Create,FileAccess.Write)) input.CopyTo(output);
|
|
146
147
|
using (var sha = SHA256.Create()) using (var file = File.OpenRead(target)) {
|
|
147
148
|
string actual = BitConverter.ToString(sha.ComputeHash(file)).Replace("-","");
|
|
148
149
|
if (!string.Equals(actual,expected,StringComparison.OrdinalIgnoreCase)) throw new IOException("An installer file failed its integrity check. Please obtain a fresh copy.");
|
|
149
150
|
}
|
|
150
|
-
hashes.Remove(
|
|
151
|
+
hashes.Remove(manifestName);
|
|
151
152
|
if (progress != null) progress((double)++count / zip.Entries.Count);
|
|
152
153
|
}
|
|
153
154
|
}
|
package/source/ShellStyles.xaml
CHANGED
|
@@ -25,11 +25,17 @@
|
|
|
25
25
|
</Style>
|
|
26
26
|
<Style TargetType="Slider">
|
|
27
27
|
<Setter Property="Height" Value="22"/>
|
|
28
|
-
<Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Slider"><Track x:Name="PART_Track" Minimum="{TemplateBinding Minimum}" Maximum="{TemplateBinding Maximum}" Value="{Binding Value,RelativeSource={RelativeSource TemplatedParent},Mode=TwoWay}" VerticalAlignment="Center">
|
|
29
|
-
<Track.DecreaseRepeatButton><RepeatButton Command="Slider.DecreaseLarge"><RepeatButton.Template><ControlTemplate TargetType="RepeatButton"><Border Background="{
|
|
30
|
-
<Track.Thumb><Thumb Width="12" Height="12"><Thumb.Template><ControlTemplate TargetType="Thumb"><Ellipse Fill="{DynamicResource Theme182C37}"/></ControlTemplate></Thumb.Template></Thumb></Track.Thumb>
|
|
31
|
-
<Track.IncreaseRepeatButton><RepeatButton Command="Slider.IncreaseLarge"><RepeatButton.Template><ControlTemplate TargetType="RepeatButton"><Border Background="{
|
|
32
|
-
</Track
|
|
28
|
+
<Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Slider"><Track x:Name="PART_Track" Orientation="{TemplateBinding Orientation}" IsDirectionReversed="{TemplateBinding IsDirectionReversed}" Minimum="{TemplateBinding Minimum}" Maximum="{TemplateBinding Maximum}" Value="{Binding Value,RelativeSource={RelativeSource TemplatedParent},Mode=TwoWay}" VerticalAlignment="Center">
|
|
29
|
+
<Track.DecreaseRepeatButton><RepeatButton x:Name="Decrease" Command="Slider.DecreaseLarge" Height="3" Background="{DynamicResource Theme006D68}"><RepeatButton.Template><ControlTemplate TargetType="RepeatButton"><Border Background="{TemplateBinding Background}" CornerRadius="1"/></ControlTemplate></RepeatButton.Template></RepeatButton></Track.DecreaseRepeatButton>
|
|
30
|
+
<Track.Thumb><Thumb x:Name="SliderThumb" Width="12" Height="12"><Thumb.Template><ControlTemplate TargetType="Thumb"><Ellipse Fill="{DynamicResource Theme182C37}"/></ControlTemplate></Thumb.Template></Thumb></Track.Thumb>
|
|
31
|
+
<Track.IncreaseRepeatButton><RepeatButton x:Name="Increase" Command="Slider.IncreaseLarge" Height="3" Background="{DynamicResource ThemeC3CED0}"><RepeatButton.Template><ControlTemplate TargetType="RepeatButton"><Border Background="{TemplateBinding Background}" CornerRadius="1"/></ControlTemplate></RepeatButton.Template></RepeatButton></Track.IncreaseRepeatButton>
|
|
32
|
+
</Track><ControlTemplate.Triggers><Trigger Property="Orientation" Value="Vertical">
|
|
33
|
+
<Setter TargetName="PART_Track" Property="VerticalAlignment" Value="Stretch"/>
|
|
34
|
+
<Setter TargetName="Decrease" Property="Height" Value="Auto"/><Setter TargetName="Decrease" Property="Width" Value="3"/>
|
|
35
|
+
<Setter TargetName="Increase" Property="Height" Value="Auto"/><Setter TargetName="Increase" Property="Width" Value="3"/>
|
|
36
|
+
<Setter TargetName="SliderThumb" Property="Width" Value="22"/><Setter TargetName="SliderThumb" Property="Height" Value="12"/>
|
|
37
|
+
<Setter TargetName="SliderThumb" Property="Template"><Setter.Value><ControlTemplate TargetType="Thumb"><Border Background="{DynamicResource Theme182C37}" BorderBrush="{DynamicResource Theme006D68}" BorderThickness="1" CornerRadius="2"><Border Height="1" Margin="3,0" Background="{DynamicResource Theme526771}"/></Border></ControlTemplate></Setter.Value></Setter>
|
|
38
|
+
</Trigger></ControlTemplate.Triggers></ControlTemplate></Setter.Value></Setter>
|
|
33
39
|
</Style>
|
|
34
40
|
<Style TargetType="TextBox">
|
|
35
41
|
<Setter Property="Background" Value="{DynamicResource ThemeFFFFFF}"/><Setter Property="Foreground" Value="{DynamicResource Theme182C37}"/>
|
package/source/TempoGrid.cs
CHANGED
|
@@ -34,6 +34,20 @@ namespace DrumBuddy
|
|
|
34
34
|
public static double StepSeconds(TempoGridOptions options) { return 240.0 / options.Tempo / options.Division * (options.Triplet ? 2.0 / 3 : 1); }
|
|
35
35
|
public static double BeatSeconds(TempoGridOptions options) { return 240.0 / options.Tempo / options.BeatUnit; }
|
|
36
36
|
public static double BarSeconds(TempoGridOptions options) { return BeatSeconds(options) * options.BeatsPerBar; }
|
|
37
|
+
public static long AdjacentBar(long frame, TempoGridOptions options, int direction)
|
|
38
|
+
{
|
|
39
|
+
options.Validate();
|
|
40
|
+
if (direction != -1 && direction != 1) throw new ArgumentOutOfRangeException("direction");
|
|
41
|
+
frame = Math.Max(0, Math.Min(EditorSession.MaxFrame, frame));
|
|
42
|
+
if (direction < 0 && frame == 0 || direction > 0 && frame == EditorSession.MaxFrame) return frame;
|
|
43
|
+
double framesPerBar = BarSeconds(options) * Audio.Rate;
|
|
44
|
+
long index = (long)Math.Floor(frame / framesPerBar);
|
|
45
|
+
if (direction > 0) {
|
|
46
|
+
do { index++; } while (FrameAt(index * framesPerBar) <= frame);
|
|
47
|
+
}
|
|
48
|
+
else while (index > 0 && FrameAt(index * framesPerBar) >= frame) index--;
|
|
49
|
+
return FrameAt(index * framesPerBar);
|
|
50
|
+
}
|
|
37
51
|
public static long Snap(long frame, TempoGridOptions options)
|
|
38
52
|
{
|
|
39
53
|
double seconds = Math.Max(0, frame) / (double)Audio.Rate, bar = BarSeconds(options), step = StepSeconds(options);
|
|
@@ -86,7 +100,7 @@ namespace DrumBuddy
|
|
|
86
100
|
int count = 0;
|
|
87
101
|
for (long index = first; index <= last && count++ < 5000; index += stride) {
|
|
88
102
|
double frame = index * seconds * Audio.Rate; if (frame > EditorSession.MaxFrame) break;
|
|
89
|
-
string label = kind == 2 && index % labelStride == 0 ? (index + 1).ToString(CultureInfo.InvariantCulture)
|
|
103
|
+
string label = kind == 2 && index % labelStride == 0 ? (index + 1).ToString(CultureInfo.InvariantCulture) : null;
|
|
90
104
|
if (kind == 1 && spacing >= 58 && index % options.BeatsPerBar != 0) label = (index / options.BeatsPerBar + 1) + "." + (index % options.BeatsPerBar + 1);
|
|
91
105
|
yield return new TempoGridTick { Frame = FrameAt(frame), Kind = kind, Label = label };
|
|
92
106
|
}
|
package/source/TempoGridTests.cs
CHANGED
|
@@ -35,6 +35,32 @@ namespace DrumBuddy
|
|
|
35
35
|
try {
|
|
36
36
|
InterfacePreferences.Current = InterfacePreferences.Defaults();
|
|
37
37
|
var grid = new TempoGridOptions(); grid.Validate();
|
|
38
|
+
Check(InterfacePreferences.Resolve(Key.Left, ModifierKeys.Control) == "previousBar" && InterfacePreferences.Resolve(Key.Right, ModifierKeys.Control) == "nextBar", "Resolve Ctrl+Left and Ctrl+Right to bar navigation");
|
|
39
|
+
Check(InterfacePreferences.Resolve(Key.Enter, ModifierKeys.None) == "startAlternate" && InterfacePreferences.Resolve(Key.Return, ModifierKeys.None) == "startAlternate" && InterfacePreferences.Resolve(Key.Home, ModifierKeys.None) == "start" && InterfacePreferences.Gesture("startAlternate") == "Enter", "Resolve Enter and Return to the beginning while retaining Home and showing Enter in keybind settings");
|
|
40
|
+
Check(TempoGrid.AdjacentBar(Audio.Rate * 3L, grid, -1) == Audio.Rate * 2L && TempoGrid.AdjacentBar(Audio.Rate * 3L, grid, 1) == Audio.Rate * 4L, "Jump from within a bar to the nearest bar strictly left or right");
|
|
41
|
+
Check(TempoGrid.AdjacentBar(Audio.Rate * 2L, grid, -1) == 0 && TempoGrid.AdjacentBar(Audio.Rate * 2L, grid, 1) == Audio.Rate * 4L, "Jump a full bar when already exactly on a bar line");
|
|
42
|
+
Check(TempoGrid.AdjacentBar(Audio.Rate * 2L - 1, grid, 1) == Audio.Rate * 2L && TempoGrid.AdjacentBar(Audio.Rate * 2L + 1, grid, -1) == Audio.Rate * 2L, "Find the neighboring bar from one sample on either side of its boundary");
|
|
43
|
+
Check(TempoGrid.AdjacentBar(0, grid, -1) == 0 && TempoGrid.AdjacentBar(EditorSession.MaxFrame, grid, 1) == EditorSession.MaxFrame && TempoGrid.AdjacentBar(EditorSession.MaxFrame - 1, grid, 1) == EditorSession.MaxFrame, "Clamp bar navigation at the beginning and timeline limit");
|
|
44
|
+
var oddMeter = new TempoGridOptions { Tempo = 123, BeatsPerBar = 7, BeatUnit = 8, Division = 256, Triplet = true };
|
|
45
|
+
double barFrames = TempoGrid.BarSeconds(oddMeter) * Audio.Rate;
|
|
46
|
+
foreach (int barIndex in new[] { 1, 2, 13, 10000, 50000 }) {
|
|
47
|
+
long barFrame = (long)Math.Round(barIndex * barFrames, MidpointRounding.AwayFromZero);
|
|
48
|
+
Check(TempoGrid.AdjacentBar(barFrame, oddMeter, -1) == (long)Math.Round((barIndex - 1) * barFrames, MidpointRounding.AwayFromZero)
|
|
49
|
+
&& TempoGrid.AdjacentBar(barFrame, oddMeter, 1) == (long)Math.Round((barIndex + 1) * barFrames, MidpointRounding.AwayFromZero)
|
|
50
|
+
&& TempoGrid.AdjacentBar(barFrame + 1, oddMeter, -1) == barFrame && TempoGrid.AdjacentBar(barFrame - 1, oddMeter, 1) == barFrame,
|
|
51
|
+
"Avoid rounded-frame drift with fractional bar lengths in 7/8 at bar " + barIndex);
|
|
52
|
+
}
|
|
53
|
+
var oldKeys = InterfacePreferences.Defaults(); oldKeys.Keybinds.RemoveAll(b => new[] { "previousBar", "nextBar", "startAlternate" }.Contains(b.Action));
|
|
54
|
+
InterfacePreferences.Upgrade(oldKeys); InterfacePreferences.Validate(oldKeys);
|
|
55
|
+
Check(oldKeys.Keybinds.Any(b => b.Action == "previousBar" && b.Key == "Left" && b.Modifiers == (int)ModifierKeys.Control) && oldKeys.Keybinds.Any(b => b.Action == "startAlternate" && b.Key == "Return"), "Add new navigation shortcuts to existing saved keymaps");
|
|
56
|
+
var customNavigation = InterfacePreferences.Defaults(); customNavigation.Keybinds.RemoveAll(b => new[] { "previousBar", "nextBar", "startAlternate" }.Contains(b.Action));
|
|
57
|
+
var customLeft = customNavigation.Keybinds.First(b => b.Action == "split"); customLeft.Key = "Left"; customLeft.Modifiers = (int)ModifierKeys.Control;
|
|
58
|
+
customNavigation.Keybinds.First(b => b.Action == "play").Key = "Enter";
|
|
59
|
+
InterfacePreferences.Upgrade(customNavigation); InterfacePreferences.Validate(customNavigation);
|
|
60
|
+
Check(customNavigation.Keybinds.First(b => b.Action == "previousBar").Key == "None" && customNavigation.Keybinds.First(b => b.Action == "startAlternate").Key == "None" && customNavigation.Keybinds.First(b => b.Action == "play").Key == "Enter", "Preserve customized Ctrl+Left and Enter bindings when upgrading a keymap");
|
|
61
|
+
var duplicateEnter = InterfacePreferences.Defaults(); duplicateEnter.Keybinds.First(b => b.Action == "play").Key = "Enter"; bool aliasRejected = false;
|
|
62
|
+
try { InterfacePreferences.Validate(duplicateEnter); } catch (IOException) { aliasRejected = true; }
|
|
63
|
+
Check(aliasRejected, "Treat Enter and Return as the same key when checking shortcut conflicts");
|
|
38
64
|
Check(Math.Abs(TempoGrid.StepSeconds(grid) - .125) < 1e-12, "At 120 BPM, a sixteenth note lasts 0.125 seconds");
|
|
39
65
|
grid.Division = 32; Check(Math.Abs(TempoGrid.StepSeconds(grid) - .0625) < 1e-12, "At 120 BPM, a thirty-second note lasts 0.0625 seconds");
|
|
40
66
|
grid.Division = 256; Check(Math.Abs(TempoGrid.StepSeconds(grid) - .0078125) < 1e-12, "Allow fine divisions through 1/256");
|
|
@@ -49,6 +75,8 @@ namespace DrumBuddy
|
|
|
49
75
|
long distant = (long)Math.Round(100000 * 240.0 / 123 / 128 * Audio.Rate);
|
|
50
76
|
Check(Math.Abs(TempoGrid.Snap(distant, grid) - distant) <= 1, "Avoid accumulated frame rounding drift far into a fine grid");
|
|
51
77
|
grid = new TempoGridOptions { Division = 32 };
|
|
78
|
+
var bars = TempoGrid.VisibleTicks(grid, 0, 400, 40).Where(t => t.Kind == 2 && t.Label != null).ToArray();
|
|
79
|
+
Check(bars.Length >= 2 && bars[0].Label == "1" && bars[1].Label == "2", "Show whole bar numbers on the musical ruler");
|
|
52
80
|
long first = TimelineGesture.FrameAt(2.5, 40, new long[] { 0 }, false, grid, true, true);
|
|
53
81
|
Check(first == 2756, "Do not swallow the first 1/32 grid line with the start-of-track magnet");
|
|
54
82
|
Check(TimelineGesture.FrameAt(21.4, 40, new long[0], false, new TempoGridOptions(), true, false) == 22050, "Snap a playhead gesture to the nearest sixteenth-note frame");
|
|
@@ -97,6 +125,18 @@ namespace DrumBuddy
|
|
|
97
125
|
editor.ApplyGridOptions(new TempoGridOptions { Tempo = 150, Division = 32 }); RunUi(() => editor.ExecuteAction("undo"));
|
|
98
126
|
Check(editor.GridOptions.Tempo == 120 && editor.SessionForTest.Regions.Any(r => r.Start == 88200), "Undo a tempo-grid change without undoing the audio paste");
|
|
99
127
|
Check(original.SequenceEqual(File.ReadAllBytes(source)), "Keep the original float audio unchanged through grid editing and copy/delete/paste");
|
|
128
|
+
editor.PlayheadForTest(Audio.Rate * 3L);
|
|
129
|
+
RunUi(() => editor.ExecuteAction(InterfacePreferences.Resolve(Key.Left, ModifierKeys.Control)));
|
|
130
|
+
Check(editor.PlayheadFrame == Audio.Rate * 2L, "Move the actual editor playhead to the left bar line");
|
|
131
|
+
RunUi(() => editor.ExecuteAction(InterfacePreferences.Resolve(Key.Left, ModifierKeys.Control)));
|
|
132
|
+
Check(editor.PlayheadFrame == 0, "Repeated Ctrl+Left moves to the preceding bar from an exact boundary");
|
|
133
|
+
RunUi(() => editor.ExecuteAction(InterfacePreferences.Resolve(Key.Right, ModifierKeys.Control)));
|
|
134
|
+
RunUi(() => editor.ExecuteAction(InterfacePreferences.Resolve(Key.Right, ModifierKeys.Control)));
|
|
135
|
+
Check(editor.PlayheadFrame == Audio.Rate * 4L, "Repeated Ctrl+Right moves the editor forward by complete bars");
|
|
136
|
+
RunUi(() => editor.ExecuteAction(InterfacePreferences.Resolve(Key.Enter, ModifierKeys.None)));
|
|
137
|
+
Check(editor.PlayheadFrame == 0, "Enter returns the actual editor playhead to the beginning");
|
|
138
|
+
editor.PlayheadForTest(Audio.Rate * 3L); RunUi(() => editor.ExecuteAction(InterfacePreferences.Resolve(Key.Home, ModifierKeys.None)));
|
|
139
|
+
Check(editor.PlayheadFrame == 0 && original.SequenceEqual(File.ReadAllBytes(source)), "Home still returns to the beginning and transport navigation preserves original audio");
|
|
100
140
|
}
|
|
101
141
|
owner.Content = null; owner.Close();
|
|
102
142
|
File.WriteAllLines(Path.Combine(root, "grid-test-results.txt"), log, Encoding.UTF8); return 0;
|
package/source/Timeline.cs
CHANGED
|
@@ -13,7 +13,7 @@ namespace DrumBuddy
|
|
|
13
13
|
{
|
|
14
14
|
public string Name = "";
|
|
15
15
|
public bool Mute, Solo;
|
|
16
|
-
public double GainDb;
|
|
16
|
+
public double GainDb, Pan;
|
|
17
17
|
public EditorTrack Copy() { return (EditorTrack)MemberwiseClone(); }
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -34,20 +34,29 @@ namespace DrumBuddy
|
|
|
34
34
|
public int Version = 1;
|
|
35
35
|
public string Name = "Untitled session";
|
|
36
36
|
public TempoGridOptions Grid = new TempoGridOptions();
|
|
37
|
+
public double MasterGainDb;
|
|
38
|
+
public bool MasterMuted;
|
|
37
39
|
public EditorTrack[] Tracks = Enumerable.Range(1, 16).Select(i => new EditorTrack { Name = "Track " + i }).ToArray();
|
|
38
40
|
public List<TimelineRegion> Regions = new List<TimelineRegion>();
|
|
39
41
|
[XmlIgnore] public long End { get { return Regions.Count == 0 ? 0 : Regions.Max(r => r.End); } }
|
|
40
42
|
public EditorSession Copy()
|
|
41
|
-
{ return new EditorSession { Version = Version, Name = Name, Grid = Grid.Copy(), Tracks = Tracks.Select(t => t.Copy()).ToArray(), Regions = Regions.Select(r => r.Copy()).ToList() }; }
|
|
43
|
+
{ return new EditorSession { Version = Version, Name = Name, Grid = Grid.Copy(), MasterGainDb = MasterGainDb, MasterMuted = MasterMuted, Tracks = Tracks.Select(t => t.Copy()).ToArray(), Regions = Regions.Select(r => r.Copy()).ToList() }; }
|
|
44
|
+
|
|
45
|
+
public void ValidateMixer()
|
|
46
|
+
{
|
|
47
|
+
if (Tracks == null || Tracks.Length != 16 || double.IsNaN(MasterGainDb) || double.IsInfinity(MasterGainDb) || MasterGainDb < -60 || MasterGainDb > 12)
|
|
48
|
+
throw new IOException("The session has an invalid master level.");
|
|
49
|
+
foreach (var track in Tracks)
|
|
50
|
+
if (track == null || double.IsNaN(track.GainDb) || double.IsInfinity(track.GainDb) || track.GainDb < -60 || track.GainDb > 24 || double.IsNaN(track.Pan) || double.IsInfinity(track.Pan) || track.Pan < -1 || track.Pan > 1)
|
|
51
|
+
throw new IOException("The session has an invalid track level or pan position.");
|
|
52
|
+
}
|
|
42
53
|
|
|
43
54
|
public void Validate()
|
|
44
55
|
{
|
|
45
56
|
if (Grid == null) Grid = new TempoGridOptions(); Grid.Validate();
|
|
46
57
|
if (Version != 1 || Tracks == null || Tracks.Length != 16 || Regions == null || Regions.Count > 20000)
|
|
47
58
|
throw new IOException("This is not a supported Flux 16-track editor session.");
|
|
48
|
-
|
|
49
|
-
if (track == null || double.IsNaN(track.GainDb) || double.IsInfinity(track.GainDb) || track.GainDb < -60 || track.GainDb > 12)
|
|
50
|
-
throw new IOException("The session has an invalid track level.");
|
|
59
|
+
ValidateMixer();
|
|
51
60
|
foreach (var r in Regions)
|
|
52
61
|
if (r == null || r.Track < 0 || r.Track >= 16 || string.IsNullOrWhiteSpace(r.Source) ||
|
|
53
62
|
r.Start < 0 || r.SourceStart < 0 || r.Length <= 0 || r.Start > MaxFrame || r.SourceStart > MaxFrame || r.Length > MaxFrame || r.End > MaxFrame || r.FadeIn < 0 || r.FadeOut < 0 || r.FadeIn > MaxFrame || r.FadeOut > MaxFrame || r.FadeInOffset < 0 || r.FadeOutOffset < 0 || r.FadeInOffset > MaxFrame || r.FadeOutOffset > MaxFrame)
|
|
@@ -140,7 +149,7 @@ namespace DrumBuddy
|
|
|
140
149
|
}
|
|
141
150
|
|
|
142
151
|
// The edit model uses integer audio frames. Rendering does not modify source files.
|
|
143
|
-
public sealed class TimelineMix : ISampleProvider, IDisposable
|
|
152
|
+
public sealed partial class TimelineMix : ISampleProvider, IDisposable
|
|
144
153
|
{
|
|
145
154
|
private sealed class Asset : IDisposable
|
|
146
155
|
{
|
|
@@ -187,11 +196,11 @@ namespace DrumBuddy
|
|
|
187
196
|
int frames = (int)Math.Min(count / 2, End - position);
|
|
188
197
|
if (frames == 0) return 0;
|
|
189
198
|
Array.Clear(output, offset, frames * 2);
|
|
199
|
+
PrepareTrackBuffers(frames * 2);
|
|
190
200
|
bool solo = session.Tracks.Any(t => t.Solo);
|
|
191
201
|
long blockEnd = position + frames;
|
|
192
202
|
foreach (var r in session.Regions) {
|
|
193
|
-
|
|
194
|
-
if (r.Muted || track.Mute || (solo && !track.Solo) || r.Start >= blockEnd || r.End <= position) continue;
|
|
203
|
+
if (r.Muted || r.Start >= blockEnd || r.End <= position) continue;
|
|
195
204
|
long from = Math.Max(r.Start, position), to = Math.Min(r.End, blockEnd);
|
|
196
205
|
int take = (int)(to - from), skip = (int)(from - position);
|
|
197
206
|
var asset = assets[r.Source]; int channels = asset.Reader.WaveFormat.Channels;
|
|
@@ -202,18 +211,35 @@ namespace DrumBuddy
|
|
|
202
211
|
while (got < byteCount && (n = asset.Reader.Read(asset.Bytes, got, byteCount - got)) > 0) got += n;
|
|
203
212
|
if (got != byteCount) throw new IOException("An audio source changed while playing. Reopen the session.");
|
|
204
213
|
Buffer.BlockCopy(asset.Bytes, 0, asset.Samples, 0, got);
|
|
205
|
-
|
|
214
|
+
var trackBuffer = trackBuffers[r.Track];
|
|
206
215
|
for (int f = 0; f < take; f++) {
|
|
207
216
|
float envelope = (float)EditorTools.Envelope(r, from + f - r.Start);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
217
|
+
trackBuffer[(skip + f) * 2] += asset.Samples[f * channels] * envelope;
|
|
218
|
+
trackBuffer[(skip + f) * 2 + 1] += asset.Samples[f * channels + (channels == 1 ? 0 : 1)] * envelope;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (int t = 0; t < 16; t++) {
|
|
222
|
+
var track = session.Tracks[t]; var trackBuffer = trackBuffers[t];
|
|
223
|
+
float gain = track.Mute || solo && !track.Solo ? 0 : (float)Math.Pow(10, track.GainDb / 20);
|
|
224
|
+
float leftGain = track.Pan >= 1 ? 0 : gain * (track.Pan > 0 ? (float)Math.Cos(track.Pan * Math.PI / 2) : 1);
|
|
225
|
+
float rightGain = track.Pan <= -1 ? 0 : gain * (track.Pan < 0 ? (float)Math.Cos(-track.Pan * Math.PI / 2) : 1);
|
|
226
|
+
for (int f = 0; f < frames; f++) {
|
|
227
|
+
float preLeft = trackBuffer[f * 2], preRight = trackBuffer[f * 2 + 1];
|
|
228
|
+
if (float.IsNaN(preLeft) || float.IsInfinity(preLeft) || float.IsNaN(preRight) || float.IsInfinity(preRight)) throw new IOException("The mix contains invalid audio samples.");
|
|
229
|
+
sourceMeterLeft[t] = Math.Max(sourceMeterLeft[t], Math.Abs(preLeft)); sourceMeterRight[t] = Math.Max(sourceMeterRight[t], Math.Abs(preRight));
|
|
230
|
+
float left = preLeft * leftGain, right = preRight * rightGain;
|
|
231
|
+
output[offset + f * 2] += left; output[offset + f * 2 + 1] += right;
|
|
232
|
+
outputMeterLeft[t] = Math.Max(outputMeterLeft[t], Math.Abs(left)); outputMeterRight[t] = Math.Max(outputMeterRight[t], Math.Abs(right));
|
|
233
|
+
TrackPeaks[t] = Math.Max(TrackPeaks[t], Math.Max(Math.Abs(left), Math.Abs(right)));
|
|
212
234
|
}
|
|
213
235
|
}
|
|
236
|
+
float masterGain = session.MasterMuted ? 0 : (float)Math.Pow(10, session.MasterGainDb / 20);
|
|
214
237
|
for (int i = offset; i < offset + frames * 2; i++) {
|
|
238
|
+
output[i] *= masterGain;
|
|
215
239
|
if (float.IsNaN(output[i]) || float.IsInfinity(output[i])) throw new IOException("The mix contains invalid audio samples.");
|
|
216
240
|
Peak = Math.Max(Peak, Math.Abs(output[i]));
|
|
241
|
+
if ((i - offset) % 2 == 0) masterMeterLeft = Math.Max(masterMeterLeft, Math.Abs(output[i]));
|
|
242
|
+
else masterMeterRight = Math.Max(masterMeterRight, Math.Abs(output[i]));
|
|
217
243
|
}
|
|
218
244
|
position += frames; return frames * 2;
|
|
219
245
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Linq;
|
|
3
|
+
|
|
4
|
+
namespace DrumBuddy
|
|
5
|
+
{
|
|
6
|
+
public sealed class TimelineMeterLevels
|
|
7
|
+
{
|
|
8
|
+
public readonly float[] SourceLeft = new float[16], SourceRight = new float[16], OutputLeft = new float[16], OutputRight = new float[16];
|
|
9
|
+
public float MasterLeft, MasterRight;
|
|
10
|
+
}
|
|
11
|
+
public sealed partial class TimelineMix
|
|
12
|
+
{
|
|
13
|
+
private readonly float[][] trackBuffers = Enumerable.Range(0, 16).Select(i => new float[0]).ToArray();
|
|
14
|
+
private readonly float[] sourceMeterLeft = new float[16], sourceMeterRight = new float[16], outputMeterLeft = new float[16], outputMeterRight = new float[16];
|
|
15
|
+
private float masterMeterLeft, masterMeterRight;
|
|
16
|
+
private void PrepareTrackBuffers(int samples)
|
|
17
|
+
{
|
|
18
|
+
for (int i = 0; i < 16; i++) {
|
|
19
|
+
if (trackBuffers[i].Length < samples) trackBuffers[i] = new float[samples];
|
|
20
|
+
else Array.Clear(trackBuffers[i], 0, samples);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
public void UpdateMixer(EditorSession state)
|
|
24
|
+
{
|
|
25
|
+
state.ValidateMixer(); var tracks = state.Tracks.Select(t => t.Copy()).ToArray();
|
|
26
|
+
double master = state.MasterGainDb; bool muted = state.MasterMuted;
|
|
27
|
+
lock (gate) {
|
|
28
|
+
if (disposed) return;
|
|
29
|
+
session.Tracks = tracks; session.MasterGainDb = master; session.MasterMuted = muted;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
public TimelineMeterLevels DrainMeters()
|
|
33
|
+
{
|
|
34
|
+
lock (gate) {
|
|
35
|
+
var result = new TimelineMeterLevels { MasterLeft = masterMeterLeft, MasterRight = masterMeterRight };
|
|
36
|
+
Array.Copy(sourceMeterLeft, result.SourceLeft, 16); Array.Copy(sourceMeterRight, result.SourceRight, 16);
|
|
37
|
+
Array.Copy(outputMeterLeft, result.OutputLeft, 16); Array.Copy(outputMeterRight, result.OutputRight, 16);
|
|
38
|
+
Array.Clear(sourceMeterLeft, 0, 16); Array.Clear(sourceMeterRight, 0, 16);
|
|
39
|
+
Array.Clear(outputMeterLeft, 0, 16); Array.Clear(outputMeterRight, 0, 16); masterMeterLeft = masterMeterRight = 0;
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
The independent C# R16 project container reader/writer uses file-format facts
|
|
2
|
-
researched with zoomrlib 1.1.0 by Remy Taymans and Glenn Boysko:
|
|
3
|
-
https://gitlab.com/remytms/zoomrlib
|
|
4
|
-
https://pypi.org/project/zoomrlib/1.1.0/
|
|
5
|
-
|
|
6
|
-
Its unchanged binary fixture projects were used for round-trip verification.
|
|
7
|
-
The upstream Python implementation is not bundled or linked into Drum Buddy.
|
|
8
|
-
Download the upstream release's tests/data to reproduce the native fixture tests:
|
|
9
|
-
DrumBuddy.exe --control-self-test TEST_OUTPUT PATH_TO_TEST_FIXTURES
|
|
10
|
-
|
|
11
|
-
Mackie control protocol reference (message assignments only):
|
|
12
|
-
https://github.com/justinfrankel/reaper-sdk/blob/main/reaper-plugins/reaper_csurf/csurf_mcu.cpp
|
|
13
|
-
|
|
14
|
-
Official R16 manuals:
|
|
15
|
-
https://zoomcorp.com/media/documents/E_R16_.pdf
|
|
16
|
-
https://zoomcorp.com/media/documents/E_R16_AIF_.pdf
|