@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/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.1.0")]
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.1\n" + InstallPath,Encoding.UTF8);
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.1"); key.SetValue("Publisher","Flux");
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(entry.FullName,out expected)) throw new IOException("An installer file is not in the integrity manifest.");
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(entry.FullName);
151
+ hashes.Remove(manifestName);
151
152
  if (progress != null) progress((double)++count / zip.Entries.Count);
152
153
  }
153
154
  }
@@ -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="{DynamicResource Theme006D68}" Height="3" CornerRadius="1"/></ControlTemplate></RepeatButton.Template></RepeatButton></Track.DecreaseRepeatButton>
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="{DynamicResource ThemeC3CED0}" Height="3" CornerRadius="1"/></ControlTemplate></RepeatButton.Template></RepeatButton></Track.IncreaseRepeatButton>
32
- </Track></ControlTemplate></Setter.Value></Setter>
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}"/>
@@ -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) + ".1" : null;
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
  }
@@ -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;
@@ -1,4 +1,4 @@
1
- Flux 1.6.1 - third-party components
1
+ Flux 1.6.4 - third-party components
2
2
 
3
3
  FFmpeg 9.0.2, release essentials build by Gyan Doshi
4
4
  Used as a separate command-line decoding process.
@@ -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
- foreach (var track in Tracks)
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
- var track = session.Tracks[r.Track];
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
- float gain = (float)Math.Pow(10, track.GainDb / 20);
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
- float left = asset.Samples[f * channels] * gain * envelope;
209
- float right = asset.Samples[f * channels + (channels == 1 ? 0 : 1)] * gain * envelope;
210
- output[offset + (skip + f) * 2] += left; output[offset + (skip + f) * 2 + 1] += right;
211
- TrackPeaks[r.Track] = Math.Max(TrackPeaks[r.Track], Math.Max(Math.Abs(left), Math.Abs(right)));
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