@pro-gram/flux 1.6.2 → 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 +15 -2
- package/dependencies.json +1 -1
- package/package.json +4 -3
- package/payload/DrumBuddy-Setup.exe +2 -2
- package/source/AssetBootstrap.cs +1 -1
- package/source/Audio Decoders.txt +1 -1
- package/source/Build Instructions.txt +5 -1
- package/source/Build.ps1 +1 -0
- 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/EditorView.cs +36 -18
- package/source/InterfaceSettings.cs +11 -4
- package/source/Main.cs +12 -8
- package/source/Main.xaml +1 -0
- package/source/MediaPlayerView.cs +1 -1
- package/source/Quick Start.txt +17 -2
- package/source/RecordingTests.cs +2 -0
- package/source/Setup.cs +3 -3
- package/source/ShellStyles.xaml +11 -5
- package/source/TempoGrid.cs +14 -0
- package/source/TempoGridTests.cs +38 -0
- package/source/Third Party Notices.txt +1 -1
- package/source/Timeline.cs +39 -13
- package/source/TimelineMixer.cs +44 -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
|
+
}
|