@pro-gram/flux 0.0.0-stage → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/NOTICE.txt +32 -0
- package/README.md +102 -2
- package/bin/flux.js +29 -0
- package/dependencies.json +14 -0
- package/package.json +60 -6
- package/payload/DrumBuddy-Setup.exe +0 -0
- package/scripts/install.js +58 -0
- package/scripts/version.js +12 -0
- package/source/AssetBootstrap.cs +49 -0
- package/source/Audio.cs +453 -0
- package/source/AudioDeviceSettings.cs +126 -0
- package/source/AudioInputs.cs +50 -0
- package/source/AudioOutputs.cs +327 -0
- package/source/AudioWorkspace.cs +71 -0
- package/source/BrandArtwork.cs +71 -0
- package/source/Build Instructions.txt +59 -0
- package/source/Build.ps1 +123 -0
- package/source/ControlMode.cs +687 -0
- package/source/ControlTests.cs +88 -0
- package/source/ConversionFormats.cs +59 -0
- package/source/ConversionTests.cs +66 -0
- package/source/DockablePane.cs +95 -0
- package/source/DriverSetup.cs +28 -0
- package/source/EditorView.cs +626 -0
- package/source/Fetch-Dependencies.ps1 +15 -0
- package/source/FloatRecording.cs +99 -0
- package/source/FloatRecordingTests.cs +170 -0
- package/source/Flux-Mark.svg +49 -0
- package/source/FluxTempo Logo Prompt.txt +3 -0
- package/source/FluxTempo-Mark.svg +49 -0
- package/source/HomeView.cs +32 -0
- package/source/InterfaceSettings.cs +219 -0
- package/source/InterfaceTests.cs +166 -0
- package/source/LICENSE.txt +23 -0
- package/source/Logo Prompt.txt +16 -0
- package/source/Main.cs +781 -0
- package/source/Main.xaml +292 -0
- package/source/MediaPlayerView.cs +274 -0
- package/source/MidiControl.cs +113 -0
- package/source/MidiPerformance.cs +125 -0
- package/source/Mixer.cs +280 -0
- package/source/NativeMemory.cs +165 -0
- package/source/PanelFeedback.cs +75 -0
- package/source/PlaybackTests.cs +134 -0
- package/source/Quick Start.txt +77 -0
- package/source/SelfTest.cs +78 -0
- package/source/SettingsWindow.cs +208 -0
- package/source/Setup.cs +192 -0
- package/source/Setup.xaml +28 -0
- package/source/ShellStyles.xaml +99 -0
- package/source/TempoGrid.cs +95 -0
- package/source/TempoGridTests.cs +108 -0
- package/source/Third Party Notices.txt +32 -0
- package/source/Timeline.cs +261 -0
- package/source/TimelineGestures.cs +57 -0
- package/source/Uninstall.cs +69 -0
- package/source/WorkspaceTests.cs +198 -0
- package/source/app.manifest +7 -0
- package/source/brand-archive/Drum-Buddy-Black-Hair.png +0 -0
- package/source/dependency-lock.json +62 -0
- package/source/fluxtempo-mark.png +0 -0
- package/source/logo.png +0 -0
- package/source/r16-panel.png +0 -0
- package/source/references/R16 Format Research.txt +16 -0
- package/test/package.test.js +55 -0
package/source/Audio.cs
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Text;
|
|
4
|
+
using System.Linq;
|
|
5
|
+
using System.Diagnostics;
|
|
6
|
+
using System.Globalization;
|
|
7
|
+
using System.Threading;
|
|
8
|
+
using System.Threading.Tasks;
|
|
9
|
+
using System.Collections.Generic;
|
|
10
|
+
using System.Collections.Concurrent;
|
|
11
|
+
using System.Text.RegularExpressions;
|
|
12
|
+
using NAudio.Wave;
|
|
13
|
+
using NAudio.Wave.SampleProviders;
|
|
14
|
+
|
|
15
|
+
namespace DrumBuddy
|
|
16
|
+
{
|
|
17
|
+
public sealed class Clip
|
|
18
|
+
{
|
|
19
|
+
public string Source, Cache, Name;
|
|
20
|
+
public string SourceFormat;
|
|
21
|
+
public string Status { get; set; }
|
|
22
|
+
public string DisplayName { get { return Name; } }
|
|
23
|
+
public string SourceSummary { get { return Ready ? SourceFormat + " | " + (Channels == 1 ? "Mono" : "Stereo") : "Reading source..."; } }
|
|
24
|
+
public int Channels, SourceRate;
|
|
25
|
+
public double Duration, Peak, Gain;
|
|
26
|
+
public float[] Waveform;
|
|
27
|
+
public bool Ready;
|
|
28
|
+
public double PeakDb { get { return Peak > 0 ? 20 * Math.Log10(Peak) : -120; } }
|
|
29
|
+
public double AppliedGain(bool protect) { return protect && Peak > 0 ? Math.Min(Gain, -1 - PeakDb) : Gain; }
|
|
30
|
+
public override string ToString() { return Name; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public static class Audio
|
|
34
|
+
{
|
|
35
|
+
public const int Rate = 44100;
|
|
36
|
+
public static string Tools { get { return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tools", "ffmpeg.exe"); } }
|
|
37
|
+
|
|
38
|
+
// Windows command-line quoting; the process never runs through a shell.
|
|
39
|
+
public static string Q(string value)
|
|
40
|
+
{
|
|
41
|
+
var s = new StringBuilder("\"");
|
|
42
|
+
int slashes = 0;
|
|
43
|
+
foreach (char ch in value)
|
|
44
|
+
{
|
|
45
|
+
if (ch == '\\') { slashes++; continue; }
|
|
46
|
+
if (ch == '"') s.Append('\\', slashes * 2 + 1).Append(ch);
|
|
47
|
+
else s.Append('\\', slashes).Append(ch);
|
|
48
|
+
slashes = 0;
|
|
49
|
+
}
|
|
50
|
+
return s.Append('\\', slashes * 2).Append('"').ToString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public static string Run(string args, CancellationToken cancel, Action<string> progress, bool expectSuccess)
|
|
54
|
+
{
|
|
55
|
+
if (!File.Exists(Tools)) throw new IOException("The audio tools are missing. Please reinstall Flux.");
|
|
56
|
+
var log = new StringBuilder();
|
|
57
|
+
var gate = new object();
|
|
58
|
+
using (var p = new Process())
|
|
59
|
+
{
|
|
60
|
+
p.StartInfo = new ProcessStartInfo(Tools, args) {
|
|
61
|
+
UseShellExecute = false, CreateNoWindow = true,
|
|
62
|
+
RedirectStandardError = true, RedirectStandardOutput = true,
|
|
63
|
+
StandardErrorEncoding = Encoding.UTF8
|
|
64
|
+
};
|
|
65
|
+
p.ErrorDataReceived += delegate(object sender, DataReceivedEventArgs e) {
|
|
66
|
+
if (e.Data == null) return;
|
|
67
|
+
lock (gate) {
|
|
68
|
+
log.AppendLine(e.Data);
|
|
69
|
+
if (log.Length > 65536) log.Remove(0, log.Length - 32768);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
p.OutputDataReceived += delegate(object sender, DataReceivedEventArgs e) {
|
|
73
|
+
if (progress != null && e.Data != null) progress(e.Data);
|
|
74
|
+
};
|
|
75
|
+
p.Start();
|
|
76
|
+
p.BeginErrorReadLine();
|
|
77
|
+
p.BeginOutputReadLine();
|
|
78
|
+
try {
|
|
79
|
+
while (!p.WaitForExit(100)) cancel.ThrowIfCancellationRequested();
|
|
80
|
+
p.WaitForExit();
|
|
81
|
+
cancel.ThrowIfCancellationRequested();
|
|
82
|
+
if (expectSuccess && p.ExitCode != 0)
|
|
83
|
+
throw new IOException("This file could not be read. It may be damaged, protected, or have no audio track.");
|
|
84
|
+
lock (gate) return log.ToString();
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
if (!p.HasExited) { p.Kill(); p.WaitForExit(); }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public static Clip Load(string path, string cacheFolder, CancellationToken cancel, Action<double> progress)
|
|
93
|
+
{
|
|
94
|
+
string info = Run("-hide_banner -nostdin -protocol_whitelist file,pipe -i " + Q(path), cancel, null, false);
|
|
95
|
+
var firstAudio = Regex.Match(info, @"Stream[^\r\n]*Audio:[^\r\n]*");
|
|
96
|
+
if (!firstAudio.Success) throw new IOException("This file has no readable audio track.");
|
|
97
|
+
int channels = Regex.IsMatch(firstAudio.Value, @"\bmono\b") ? 1 : 2;
|
|
98
|
+
var time = Regex.Match(info, @"Duration:\s*(\d+):(\d+):(\d+(?:\.\d+)?)");
|
|
99
|
+
double seconds = time.Success ? int.Parse(time.Groups[1].Value) * 3600 + int.Parse(time.Groups[2].Value) * 60 + double.Parse(time.Groups[3].Value, CultureInfo.InvariantCulture) : 0;
|
|
100
|
+
Directory.CreateDirectory(cacheFolder);
|
|
101
|
+
string cache = Path.Combine(cacheFolder, Guid.NewGuid().ToString("N") + ".wav");
|
|
102
|
+
try {
|
|
103
|
+
Run("-hide_banner -v error -nostdin -y -protocol_whitelist file,pipe -i " + Q(path) +
|
|
104
|
+
" -map 0:a:0 -vn -sn -dn -map_metadata -1 -ar 44100 -ac " + channels +
|
|
105
|
+
" -c:a pcm_f32le -rf64 auto -progress pipe:1 " + Q(cache), cancel,
|
|
106
|
+
delegate(string line) {
|
|
107
|
+
if (progress == null || seconds <= 0 || !line.StartsWith("out_time_us=")) return;
|
|
108
|
+
double us;
|
|
109
|
+
if (double.TryParse(line.Substring(12), NumberStyles.Float, CultureInfo.InvariantCulture, out us))
|
|
110
|
+
progress(Math.Min(.94, us / 1000000 / seconds * .94));
|
|
111
|
+
}, true);
|
|
112
|
+
using (var reader = new WaveFileReader(cache)) {
|
|
113
|
+
if (reader.WaveFormat.BitsPerSample != 32 || reader.WaveFormat.Channels != channels)
|
|
114
|
+
throw new IOException("The decoded audio format was unexpected.");
|
|
115
|
+
long samples = reader.Length / 4;
|
|
116
|
+
if (samples == 0) throw new IOException("This file contains no audio samples.");
|
|
117
|
+
var waveform = new float[900];
|
|
118
|
+
byte[] bytes = new byte[65536];
|
|
119
|
+
float[] floats = new float[bytes.Length / 4];
|
|
120
|
+
long position = 0;
|
|
121
|
+
double peak = 0;
|
|
122
|
+
int count;
|
|
123
|
+
while ((count = reader.Read(bytes, 0, bytes.Length)) > 0) {
|
|
124
|
+
cancel.ThrowIfCancellationRequested();
|
|
125
|
+
Buffer.BlockCopy(bytes, 0, floats, 0, count);
|
|
126
|
+
for (int i = 0; i < count / 4; i++, position++) {
|
|
127
|
+
double value = Math.Abs((double)floats[i]);
|
|
128
|
+
if (double.IsNaN(value) || double.IsInfinity(value)) throw new IOException("This file contains invalid audio samples.");
|
|
129
|
+
peak = Math.Max(peak, value);
|
|
130
|
+
int bin = (int)Math.Min(waveform.Length - 1, (double)position / samples * waveform.Length);
|
|
131
|
+
waveform[bin] = Math.Max(waveform[bin], (float)value);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (progress != null) progress(1);
|
|
135
|
+
var codec = Regex.Match(firstAudio.Value, @"Audio:\s*([^\s,(]+)").Groups[1].Value;
|
|
136
|
+
var sourceRate = Regex.Match(firstAudio.Value, @"\b(\d+)\s*Hz").Groups[1].Value;
|
|
137
|
+
string format = codec;
|
|
138
|
+
if (codec.StartsWith("pcm_f32")) format = "32-bit float WAV/PCM";
|
|
139
|
+
else if (codec.StartsWith("pcm_f64")) format = "64-bit float WAV/PCM";
|
|
140
|
+
else if (codec.StartsWith("pcm_s24")) format = "24-bit integer PCM";
|
|
141
|
+
else if (codec.StartsWith("pcm_s32")) format = "32-bit integer PCM";
|
|
142
|
+
else if (codec.StartsWith("pcm_s16")) format = "16-bit integer PCM";
|
|
143
|
+
else if (codec.StartsWith("pcm_")) format = codec.Replace("pcm_", "PCM ");
|
|
144
|
+
return new Clip { Source = path, Cache = cache, Name = Path.GetFileName(path), Channels = channels, SourceRate = sourceRate.Length > 0 ? int.Parse(sourceRate, CultureInfo.InvariantCulture) : Audio.Rate, SourceFormat = format + (sourceRate.Length > 0 ? " | " + sourceRate + " Hz" : ""),
|
|
145
|
+
Duration = samples / (double)(Rate * channels), Peak = peak, Waveform = waveform, Ready = true, Status = "Ready" };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch { TryDelete(cache); throw; }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public static string SafeName(string source)
|
|
152
|
+
{
|
|
153
|
+
string stem = Path.GetFileNameWithoutExtension(source).ToUpperInvariant();
|
|
154
|
+
stem = Regex.Replace(stem, "[^A-Z0-9_]", "_");
|
|
155
|
+
stem = Regex.Replace(stem, "_+", "_").Trim('_');
|
|
156
|
+
if (stem.Length == 0) stem = "TRACK";
|
|
157
|
+
if (Regex.IsMatch(stem,@"^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$")) stem = "_" + stem;
|
|
158
|
+
return stem.Substring(0, Math.Min(8, stem.Length));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public static string UniquePath(string folder, string stem)
|
|
162
|
+
{
|
|
163
|
+
stem = SafeName(stem + ".WAV");
|
|
164
|
+
for (int i = 0; i < 1000000; i++) {
|
|
165
|
+
string suffix = i == 0 ? "" : i.ToString(CultureInfo.InvariantCulture);
|
|
166
|
+
string name = stem.Substring(0, Math.Min(stem.Length, 8 - suffix.Length)) + suffix + ".WAV";
|
|
167
|
+
string path = Path.Combine(folder, name);
|
|
168
|
+
if (!File.Exists(path)) return path;
|
|
169
|
+
}
|
|
170
|
+
throw new IOException("This folder has too many files with similar names. Please choose another folder.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public static string Export(Clip clip, string folder, int bits, bool protect, CancellationToken cancel, Action<double> progress)
|
|
174
|
+
{
|
|
175
|
+
Directory.CreateDirectory(folder);
|
|
176
|
+
string temporary = Path.Combine(folder, ".drumbuddy-" + Guid.NewGuid().ToString("N") + ".partial");
|
|
177
|
+
try {
|
|
178
|
+
using (var input = new WaveFileReader(clip.Cache))
|
|
179
|
+
using (var output = new PcmWriter(temporary, bits, clip.Channels)) {
|
|
180
|
+
if (input.Length / 4 * (bits / 8) > PcmWriter.MaxData) throw new IOException("This recording is too long for an R16 WAV file. Please use a shorter recording.");
|
|
181
|
+
byte[] bytes = new byte[65536];
|
|
182
|
+
float[] samples = new float[bytes.Length / 4];
|
|
183
|
+
double scale = Math.Pow(10, clip.AppliedGain(protect) / 20);
|
|
184
|
+
int count;
|
|
185
|
+
while ((count = input.Read(bytes, 0, bytes.Length)) > 0) {
|
|
186
|
+
cancel.ThrowIfCancellationRequested();
|
|
187
|
+
Buffer.BlockCopy(bytes, 0, samples, 0, count);
|
|
188
|
+
output.Write(samples, count / 4, scale);
|
|
189
|
+
if (progress != null) progress((double)input.Position / input.Length);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
cancel.ThrowIfCancellationRequested();
|
|
193
|
+
for (int attempt = 0; attempt < 100; attempt++) {
|
|
194
|
+
string final = UniquePath(folder, Path.GetFileNameWithoutExtension(clip.Source));
|
|
195
|
+
try { File.Move(temporary, final); return final; }
|
|
196
|
+
catch (IOException) { if (!File.Exists(final)) throw; }
|
|
197
|
+
}
|
|
198
|
+
throw new IOException("A file with this name is already being saved. Try again.");
|
|
199
|
+
}
|
|
200
|
+
finally { TryDelete(temporary); }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public static void TryDelete(string path) { try { if (File.Exists(path)) File.Delete(path); } catch { } }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Classic 44-byte PCM headers are intentional: compatible with older recorders.
|
|
207
|
+
public sealed class PcmWriter : IRecordingWriter
|
|
208
|
+
{
|
|
209
|
+
public const long MaxData = 0xFFFFFF00L;
|
|
210
|
+
private readonly FileStream stream;
|
|
211
|
+
private readonly BinaryWriter writer;
|
|
212
|
+
private readonly int bits, channels, rate;
|
|
213
|
+
private readonly Random random = new Random();
|
|
214
|
+
private byte[] buffer = new byte[0];
|
|
215
|
+
private long dataSize;
|
|
216
|
+
private bool disposed;
|
|
217
|
+
public PcmWriter(string path, int bits, int channels) : this(path, bits, channels, Audio.Rate) { }
|
|
218
|
+
public PcmWriter(string path, int bits, int channels, int rate)
|
|
219
|
+
{
|
|
220
|
+
if ((bits != 16 && bits != 24) || channels < 1 || channels > 2 || (rate != 44100 && rate != 48000 && rate != 96000)) throw new ArgumentException("Invalid WAV format");
|
|
221
|
+
this.bits = bits; this.channels = channels; this.rate = rate;
|
|
222
|
+
stream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read, 65536);
|
|
223
|
+
writer = new BinaryWriter(stream);
|
|
224
|
+
writer.Write(Encoding.ASCII.GetBytes("RIFF")); writer.Write((uint)36);
|
|
225
|
+
writer.Write(Encoding.ASCII.GetBytes("WAVEfmt ")); writer.Write(16);
|
|
226
|
+
writer.Write((ushort)1); writer.Write((ushort)channels); writer.Write(rate);
|
|
227
|
+
writer.Write(rate * channels * bits / 8); writer.Write((ushort)(channels * bits / 8)); writer.Write((ushort)bits);
|
|
228
|
+
writer.Write(Encoding.ASCII.GetBytes("data")); writer.Write((uint)0);
|
|
229
|
+
}
|
|
230
|
+
public void Write(float[] samples, int count, double gain)
|
|
231
|
+
{
|
|
232
|
+
if (count % channels != 0) throw new ArgumentException("Incomplete audio frame");
|
|
233
|
+
int size = count * (bits / 8);
|
|
234
|
+
if (dataSize + size > MaxData) throw new IOException("The WAV file has reached its size limit. This take has been stopped and saved.");
|
|
235
|
+
if (buffer.Length < size) buffer = new byte[size];
|
|
236
|
+
int scale = 1 << (bits - 1), j = 0;
|
|
237
|
+
for (int i = 0; i < count; i++) {
|
|
238
|
+
double value = samples[i] * gain;
|
|
239
|
+
if (double.IsNaN(value) || double.IsInfinity(value)) value = 0;
|
|
240
|
+
double dither = bits == 16 ? random.NextDouble() - random.NextDouble() : 0;
|
|
241
|
+
double quantized = Math.Round(value * scale + dither, MidpointRounding.AwayFromZero);
|
|
242
|
+
int pcm = (int)Math.Max(-scale, Math.Min(scale - 1, quantized));
|
|
243
|
+
buffer[j++] = (byte)pcm; buffer[j++] = (byte)(pcm >> 8);
|
|
244
|
+
if (bits == 24) buffer[j++] = (byte)(pcm >> 16);
|
|
245
|
+
}
|
|
246
|
+
stream.Write(buffer, 0, size); dataSize += size;
|
|
247
|
+
// Keep an in-progress take readable if Windows or the app stops unexpectedly.
|
|
248
|
+
if ((dataSize / (rate * channels * (bits / 8))) != ((dataSize - size) / (rate * channels * (bits / 8)))) UpdateHeader();
|
|
249
|
+
}
|
|
250
|
+
private void UpdateHeader()
|
|
251
|
+
{
|
|
252
|
+
long position = stream.Position;
|
|
253
|
+
stream.Position = 4; writer.Write((uint)(36 + dataSize + (dataSize & 1)));
|
|
254
|
+
stream.Position = 40; writer.Write((uint)dataSize);
|
|
255
|
+
stream.Position = position; stream.Flush();
|
|
256
|
+
}
|
|
257
|
+
public void Dispose() { if (disposed) return; disposed = true; try { if ((dataSize & 1) != 0) stream.WriteByte(0); UpdateHeader(); } finally { writer.Dispose(); } }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
public sealed class GainProvider : ISampleProvider
|
|
261
|
+
{
|
|
262
|
+
private readonly ISampleProvider source;
|
|
263
|
+
public volatile float Gain = 1;
|
|
264
|
+
public GainProvider(ISampleProvider source) { this.source = source; }
|
|
265
|
+
public WaveFormat WaveFormat { get { return source.WaveFormat; } }
|
|
266
|
+
public int Read(float[] buffer, int offset, int count)
|
|
267
|
+
{
|
|
268
|
+
int read = source.Read(buffer, offset, count);
|
|
269
|
+
float gain = Gain;
|
|
270
|
+
for (int i = offset; i < offset + read; i++) buffer[i] *= gain;
|
|
271
|
+
return read;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
public sealed class Recorder
|
|
276
|
+
{
|
|
277
|
+
private AsioOut device;
|
|
278
|
+
private WaveInEvent windowsInput;
|
|
279
|
+
private int windowsChannels;
|
|
280
|
+
private BlockingCollection<float[]> queue;
|
|
281
|
+
private readonly ConcurrentQueue<float[]> pool = new ConcurrentQueue<float[]>();
|
|
282
|
+
private Task writing;
|
|
283
|
+
private readonly object gate = new object();
|
|
284
|
+
private bool accepting;
|
|
285
|
+
private int captureChannels, selectedInput, savedChannels;
|
|
286
|
+
private bool separate, requireFloatInput;
|
|
287
|
+
private readonly List<IRecordingWriter> writers = new List<IRecordingWriter>();
|
|
288
|
+
public readonly List<string> Paths = new List<string>();
|
|
289
|
+
public readonly float[] Peaks = new float[8];
|
|
290
|
+
public Exception Error;
|
|
291
|
+
public long Frames;
|
|
292
|
+
public int SampleRate { get; private set; }
|
|
293
|
+
public int Bits { get; private set; }
|
|
294
|
+
public int ChannelCount { get { return captureChannels; } }
|
|
295
|
+
public string SourceFormat { get; private set; }
|
|
296
|
+
public bool SourceIsFloat { get; private set; }
|
|
297
|
+
|
|
298
|
+
public void Start(string driver, int mode, int bits, string folder)
|
|
299
|
+
{ Start(driver, mode, bits, folder, Audio.Rate); }
|
|
300
|
+
|
|
301
|
+
public void Start(InputChoice input, int mode, int bits, string folder, int rate)
|
|
302
|
+
{
|
|
303
|
+
if (input.Driver != null) { Start(input.Driver, mode, bits, folder, rate); return; }
|
|
304
|
+
try {
|
|
305
|
+
windowsChannels = AudioInputs.Channels(input);
|
|
306
|
+
var layout = CaptureLayout.Create(windowsChannels, mode);
|
|
307
|
+
windowsInput = new WaveInEvent { DeviceNumber = AudioInputs.Device(input), WaveFormat = new WaveFormat(rate, 16, windowsChannels), BufferMilliseconds = 50, NumberOfBuffers = 3 };
|
|
308
|
+
StartBuffered(layout, bits, folder, rate, false);
|
|
309
|
+
SourceFormat = "Windows 16-bit PCM"; SourceIsFloat = false;
|
|
310
|
+
windowsInput.DataAvailable += delegate(object sender, WaveInEventArgs e) { ProcessWindowsPcm(e.Buffer, e.BytesRecorded, windowsChannels); };
|
|
311
|
+
windowsInput.RecordingStopped += delegate(object sender, StoppedEventArgs e) {
|
|
312
|
+
lock (gate) { if (accepting && Error == null) Error = e.Exception ?? new IOException("The microphone stopped. Check its connection and Windows microphone permissions."); }
|
|
313
|
+
};
|
|
314
|
+
windowsInput.StartRecording();
|
|
315
|
+
}
|
|
316
|
+
catch { Stop(); throw; }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
internal void ProcessWindowsPcm(byte[] data, int bytes, int sourceChannels)
|
|
320
|
+
{
|
|
321
|
+
lock (gate) {
|
|
322
|
+
if (!accepting || Error != null) return;
|
|
323
|
+
try {
|
|
324
|
+
if (requireFloatInput) throw new IOException("Windows PCM input cannot supply native ASIO float samples.");
|
|
325
|
+
if (sourceChannels < 1 || bytes < 0 || bytes > data.Length || bytes % (sourceChannels * 2) != 0 || selectedInput + captureChannels > sourceChannels)
|
|
326
|
+
throw new IOException("The Windows microphone sent an incomplete or changed channel layout.");
|
|
327
|
+
int frames = bytes / (sourceChannels * 2), count = frames * captureChannels;
|
|
328
|
+
float[] samples;
|
|
329
|
+
if (!pool.TryDequeue(out samples) || samples.Length != count) samples = new float[count];
|
|
330
|
+
for (int f = 0; f < frames; f++) for (int c = 0; c < captureChannels; c++) {
|
|
331
|
+
float value = BitConverter.ToInt16(data, (f * sourceChannels + selectedInput + c) * 2) / 32768f;
|
|
332
|
+
samples[f * captureChannels + c] = value;
|
|
333
|
+
Peaks[selectedInput + c] = Math.Max(Peaks[selectedInput + c], Math.Abs(value));
|
|
334
|
+
}
|
|
335
|
+
SourceFormat = "Windows 16-bit PCM"; SourceIsFloat = false;
|
|
336
|
+
if (!queue.TryAdd(samples)) throw new IOException("The computer could not save microphone audio fast enough. Stop and save the take.");
|
|
337
|
+
Interlocked.Add(ref Frames, frames);
|
|
338
|
+
}
|
|
339
|
+
catch (Exception ex) { Error = ex; }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
public void Start(string driver, int mode, int bits, string folder, int rate)
|
|
344
|
+
{
|
|
345
|
+
try {
|
|
346
|
+
device = new AsioOut(driver);
|
|
347
|
+
var layout = CaptureLayout.Create(device.DriverInputChannelCount, mode);
|
|
348
|
+
if (!device.IsSampleRateSupported(rate)) throw new IOException("This device does not support " + rate + " Hz in its current mode. Choose a supported rate, or set the recorder to Audio I/F Only.");
|
|
349
|
+
device.InputChannelOffset = layout.FirstInput;
|
|
350
|
+
device.InitRecordAndPlayback(null, layout.Channels, rate);
|
|
351
|
+
StartBuffered(layout, bits, folder, rate, bits == 32 && CaptureLayout.IsH5studio(driver));
|
|
352
|
+
device.AudioAvailable += Captured;
|
|
353
|
+
device.PlaybackStopped += delegate(object sender, StoppedEventArgs e) {
|
|
354
|
+
lock (gate) { if (accepting && Error == null) Error = e.Exception ?? new IOException("The recording device stopped sending audio. Check its USB connection."); }
|
|
355
|
+
};
|
|
356
|
+
device.Play();
|
|
357
|
+
}
|
|
358
|
+
catch { Stop(); throw; }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Shared by real ASIO capture and the offline driver-buffer acceptance checks.
|
|
362
|
+
internal void StartBuffered(CaptureLayout layout, int bits, string folder, int rate, bool requireNativeFloat)
|
|
363
|
+
{
|
|
364
|
+
if (bits != 16 && bits != 24 && bits != 32) throw new ArgumentException("Unsupported recording format.");
|
|
365
|
+
separate = layout.Separate; selectedInput = layout.FirstInput; captureChannels = layout.Channels;
|
|
366
|
+
savedChannels = separate ? 1 : captureChannels; SampleRate = rate; Bits = bits; requireFloatInput = requireNativeFloat;
|
|
367
|
+
Directory.CreateDirectory(folder);
|
|
368
|
+
for (int i = 0; i < (separate ? captureChannels : 1); i++) {
|
|
369
|
+
string name = separate ? "INPUT_" + (i + selectedInput + 1) : captureChannels == 2 ? "STEREO" : "INPUT_" + (selectedInput + 1);
|
|
370
|
+
string path = Path.Combine(folder, name + ".WAV");
|
|
371
|
+
IRecordingWriter writer = bits == 32 ? (IRecordingWriter)new FloatWaveWriter(path, savedChannels, rate) : new PcmWriter(path, bits, savedChannels, rate);
|
|
372
|
+
writers.Add(writer); Paths.Add(path);
|
|
373
|
+
}
|
|
374
|
+
queue = new BlockingCollection<float[]>(128);
|
|
375
|
+
writing = Task.Run((Action)WriteLoop); accepting = true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
internal void ProcessCapture(AsioAudioAvailableEventArgs e) { Captured(this, e); }
|
|
379
|
+
private void Captured(object sender, AsioAudioAvailableEventArgs e)
|
|
380
|
+
{
|
|
381
|
+
lock (gate) {
|
|
382
|
+
if (!accepting || Error != null) return;
|
|
383
|
+
try {
|
|
384
|
+
SourceFormat = e.AsioSampleType.ToString();
|
|
385
|
+
SourceIsFloat = e.AsioSampleType == NAudio.Wave.Asio.AsioSampleType.Float32LSB;
|
|
386
|
+
if (requireFloatInput && !SourceIsFloat)
|
|
387
|
+
throw new IOException("The H5studio driver is sending " + SourceFormat + ", not native 32-bit float. Check Zoom's H5studio driver and its audio settings, then reconnect. No integer input has been labelled as a native float recording.");
|
|
388
|
+
if (e.InputBuffers.Length != captureChannels || e.SamplesPerBuffer <= 0)
|
|
389
|
+
throw new IOException("The ASIO input channel layout changed. Stop, refresh the device, and start a new take.");
|
|
390
|
+
int count = e.SamplesPerBuffer * captureChannels;
|
|
391
|
+
float[] samples;
|
|
392
|
+
if (!pool.TryDequeue(out samples) || samples.Length != count) samples = new float[count];
|
|
393
|
+
e.GetAsInterleavedSamples(samples);
|
|
394
|
+
for (int i = 0; i < count; i++)
|
|
395
|
+
if (float.IsNaN(samples[i]) || float.IsInfinity(samples[i])) throw new IOException("The driver sent invalid audio. The previous audio blocks have been kept.");
|
|
396
|
+
for (int channel = 0; channel < captureChannels; channel++) {
|
|
397
|
+
float peak = 0;
|
|
398
|
+
for (int i = channel; i < count; i += captureChannels) peak = Math.Max(peak, Math.Abs(samples[i]));
|
|
399
|
+
int input = channel + selectedInput;
|
|
400
|
+
Peaks[input] = Math.Max(Peaks[input], peak);
|
|
401
|
+
}
|
|
402
|
+
if (!queue.TryAdd(samples)) throw new IOException("The computer could not save audio fast enough. The take has been stopped and saved. Close other apps before trying again.");
|
|
403
|
+
Interlocked.Add(ref Frames, e.SamplesPerBuffer);
|
|
404
|
+
}
|
|
405
|
+
catch (Exception ex) { Error = ex; }
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
private void WriteLoop()
|
|
409
|
+
{
|
|
410
|
+
try {
|
|
411
|
+
float[] mono = new float[0];
|
|
412
|
+
foreach (float[] block in queue.GetConsumingEnumerable()) {
|
|
413
|
+
if (separate) {
|
|
414
|
+
int frames = block.Length / captureChannels;
|
|
415
|
+
if (mono.Length < frames) mono = new float[frames];
|
|
416
|
+
for (int c = 0; c < captureChannels; c++) {
|
|
417
|
+
for (int f = 0; f < frames; f++) mono[f] = block[f * captureChannels + c];
|
|
418
|
+
writers[c].Write(mono, frames, 1);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
else writers[0].Write(block, block.Length, 1);
|
|
422
|
+
pool.Enqueue(block);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch (Exception ex) { Error = ex; }
|
|
426
|
+
}
|
|
427
|
+
// Call on the UI thread: ASIO COM objects have thread affinity.
|
|
428
|
+
public void Stop()
|
|
429
|
+
{
|
|
430
|
+
lock (gate) accepting = false;
|
|
431
|
+
if (windowsInput != null) {
|
|
432
|
+
try { windowsInput.StopRecording(); } catch (Exception ex) { if (Error == null) Error = ex; }
|
|
433
|
+
try { windowsInput.Dispose(); } catch (Exception ex) { if (Error == null) Error = ex; }
|
|
434
|
+
windowsInput = null;
|
|
435
|
+
}
|
|
436
|
+
if (device != null) {
|
|
437
|
+
try { device.Stop(); } catch (Exception ex) { if (Error == null) Error = ex; }
|
|
438
|
+
try { device.Dispose(); } catch (Exception ex) { if (Error == null) Error = ex; }
|
|
439
|
+
device = null;
|
|
440
|
+
}
|
|
441
|
+
if (queue != null && !queue.IsAddingCompleted) queue.CompleteAdding();
|
|
442
|
+
}
|
|
443
|
+
public async Task Finish()
|
|
444
|
+
{
|
|
445
|
+
if (writing != null) await writing;
|
|
446
|
+
foreach (var writer in writers) {
|
|
447
|
+
try { writer.Dispose(); } catch (Exception ex) { if (Error == null) Error = ex; }
|
|
448
|
+
}
|
|
449
|
+
writers.Clear();
|
|
450
|
+
if (queue != null) { queue.Dispose(); queue = null; }
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Text;
|
|
5
|
+
using System.Collections.Generic;
|
|
6
|
+
using System.Reflection;
|
|
7
|
+
using System.Windows;
|
|
8
|
+
using System.Windows.Controls;
|
|
9
|
+
using NAudio.CoreAudioApi;
|
|
10
|
+
using NAudio.Wave;
|
|
11
|
+
|
|
12
|
+
namespace DrumBuddy
|
|
13
|
+
{
|
|
14
|
+
public sealed class AudioDevicesView : StackPanel, IDisposable
|
|
15
|
+
{
|
|
16
|
+
private readonly AudioOutputPicker outputs;
|
|
17
|
+
private readonly ComboBox inputs;
|
|
18
|
+
private readonly TextBlock result;
|
|
19
|
+
private readonly Action<InputChoice> inputChanged;
|
|
20
|
+
private readonly Action stopPlayback;
|
|
21
|
+
private PlaybackRoute test;
|
|
22
|
+
private bool refreshing;
|
|
23
|
+
public AudioDevicesView(InputChoice selectedInput, Action<InputChoice> inputChanged, Action stopPlayback, bool recording)
|
|
24
|
+
{
|
|
25
|
+
this.inputChanged = inputChanged; this.stopPlayback = stopPlayback;
|
|
26
|
+
Margin = new Thickness(24); Background = WorkspaceUi.Color("#F3F5F5");
|
|
27
|
+
Children.Add(WorkspaceUi.Text("Audio devices", 28));
|
|
28
|
+
var description = WorkspaceUi.Text("Choose where playback is heard and which input is recorded. These selections are saved for your next session.", 15);
|
|
29
|
+
description.Margin = new Thickness(0, 10, 0, 20); Children.Add(description);
|
|
30
|
+
Children.Add(WorkspaceUi.Text("Speakers / headphones", 18));
|
|
31
|
+
outputs = new AudioOutputPicker(delegate { StopTest(); if (stopPlayback != null) stopPlayback(); }, delegate(string text) { if (result != null) result.Text = text; });
|
|
32
|
+
outputs.Margin = new Thickness(0, 8, 0, 4); Children.Add(outputs);
|
|
33
|
+
var help = WorkspaceUi.Text("For a Realtek or Windows audio error, choose that device's Compatibility (16-bit PCM) entry. Automatic Windows output also tries this path if shared audio cannot open.", 14);
|
|
34
|
+
help.Margin = new Thickness(0, 0, 0, 10); Children.Add(help);
|
|
35
|
+
Children.Add(WorkspaceUi.Button("Test speakers", Test));
|
|
36
|
+
Children.Add(WorkspaceUi.Text("Recording input", 18));
|
|
37
|
+
inputs = new ComboBox { MinHeight = 38, FontSize = 15, Margin = new Thickness(0, 8, 0, 8), IsEnabled = !recording };
|
|
38
|
+
Children.Add(inputs); RefreshInput(selectedInput == null ? null : selectedInput.Key);
|
|
39
|
+
inputs.SelectionChanged += delegate { if (!refreshing && inputs.SelectedItem != null && this.inputChanged != null) this.inputChanged((InputChoice)inputs.SelectedItem); };
|
|
40
|
+
var refresh = WorkspaceUi.Button("Refresh inputs", delegate { var current = inputs.SelectedItem as InputChoice; RefreshInput(current == null ? null : current.Key); }); refresh.IsEnabled = !recording; Children.Add(refresh);
|
|
41
|
+
var captureHelp = WorkspaceUi.Text(recording ? "Stop and save the current take before changing its recording input." : "Windows microphones use 16-bit PCM input. Choose the H5studio ASIO input for native 32-bit float capture. Channel and WAV settings are in Record drums.", 14);
|
|
42
|
+
captureHelp.Margin = new Thickness(0, 0, 0, 14); Children.Add(captureHelp);
|
|
43
|
+
var buttons = new WrapPanel(); Children.Add(buttons);
|
|
44
|
+
buttons.Children.Add(WorkspaceUi.Button("Save diagnostics...", delegate {
|
|
45
|
+
var dialog = new Microsoft.Win32.SaveFileDialog { Title = "Save audio device diagnostics", Filter = "Text report|*.txt", FileName = "DrumBuddy-Audio-Diagnostics.txt" };
|
|
46
|
+
if (dialog.ShowDialog(Window.GetWindow(this)) == true) try {
|
|
47
|
+
File.WriteAllText(dialog.FileName, AudioDiagnostics.Report(inputs.SelectedItem as InputChoice), Encoding.UTF8);
|
|
48
|
+
result.Text = "Diagnostics saved. The report contains device formats and recent playback errors; it contains no recordings.";
|
|
49
|
+
} catch (Exception ex) { result.Text = "Could not save diagnostics: " + ex.Message; }
|
|
50
|
+
}));
|
|
51
|
+
result = WorkspaceUi.Text("Test speakers plays a short quiet tone through the selected output. No microphone is opened until you press Record.", 14); result.Margin = new Thickness(0, 10, 0, 0); Children.Add(result);
|
|
52
|
+
}
|
|
53
|
+
private void RefreshInput(string key)
|
|
54
|
+
{
|
|
55
|
+
refreshing = true;
|
|
56
|
+
var choices = AudioInputs.Choices(); inputs.ItemsSource = choices;
|
|
57
|
+
inputs.SelectedItem = choices.FirstOrDefault(c => c.Key == key);
|
|
58
|
+
refreshing = false;
|
|
59
|
+
}
|
|
60
|
+
private void Test()
|
|
61
|
+
{
|
|
62
|
+
StopTest(); if (stopPlayback != null) stopPlayback();
|
|
63
|
+
try {
|
|
64
|
+
test = AudioOutputs.Open(new TestSignal(), outputs.Selected); var current = test;
|
|
65
|
+
test.Player.PlaybackStopped += delegate(object sender, StoppedEventArgs e) {
|
|
66
|
+
Dispatcher.BeginInvoke((Action)delegate {
|
|
67
|
+
if (test != current) return;
|
|
68
|
+
string description = test.Description; StopTest();
|
|
69
|
+
result.Text = e.Exception == null ? "Test tone sent through " + description + "." : "Test stopped: " + e.Exception.Message;
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
result.Text = "Playing test through " + test.Description; test.Player.Play();
|
|
73
|
+
}
|
|
74
|
+
catch (Exception ex) { StopTest(); result.Text = ex.Message; }
|
|
75
|
+
}
|
|
76
|
+
private void StopTest() { var old = test; test = null; if (old != null) old.Dispose(); }
|
|
77
|
+
public void Dispose() { StopTest(); }
|
|
78
|
+
private sealed class TestSignal : ISampleProvider
|
|
79
|
+
{
|
|
80
|
+
private int frame;
|
|
81
|
+
public WaveFormat WaveFormat { get { return WaveFormat.CreateIeeeFloatWaveFormat(44100, 2); } }
|
|
82
|
+
public int Read(float[] buffer, int offset, int count)
|
|
83
|
+
{
|
|
84
|
+
int frames = Math.Min(count / 2, 15435 - frame);
|
|
85
|
+
for (int i = 0; i < frames; i++, frame++) {
|
|
86
|
+
double fade = Math.Min(1, Math.Min(frame / 441.0, (15435 - frame) / 441.0));
|
|
87
|
+
float value = (float)(.04 * fade * Math.Sin(frame * 2 * Math.PI * 440 / 44100));
|
|
88
|
+
buffer[offset + i * 2] = buffer[offset + i * 2 + 1] = value;
|
|
89
|
+
}
|
|
90
|
+
return frames * 2;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public static class AudioDiagnostics
|
|
96
|
+
{
|
|
97
|
+
public static string Report(InputChoice input)
|
|
98
|
+
{
|
|
99
|
+
var lines = new List<string> {
|
|
100
|
+
"Flux audio diagnostics", "App: " + Assembly.GetExecutingAssembly().GetName().Version,
|
|
101
|
+
"Windows: " + Environment.OSVersion, "Output selection: " + AudioOutputs.PreferredKey,
|
|
102
|
+
"Input selection: " + (input == null ? "not selected" : input.Name + " / " + input.Key), "",
|
|
103
|
+
"This report reads device information only. It does not record a microphone, play audio, change system volume or install a driver." };
|
|
104
|
+
try {
|
|
105
|
+
using (var enumerator = new MMDeviceEnumerator()) {
|
|
106
|
+
foreach (DataFlow flow in new[] { DataFlow.Render, DataFlow.Capture }) {
|
|
107
|
+
foreach (Role role in new[] { Role.Console, Role.Multimedia, Role.Communications })
|
|
108
|
+
try { using (var device = enumerator.GetDefaultAudioEndpoint(flow, role)) lines.Add(flow + " default " + role + ": " + device.FriendlyName + " / " + device.ID); }
|
|
109
|
+
catch (Exception ex) { lines.Add(flow + " default " + role + ": " + ex.Message); }
|
|
110
|
+
foreach (var device in enumerator.EnumerateAudioEndPoints(flow, DeviceState.All)) using (device) {
|
|
111
|
+
lines.Add(""); lines.Add(flow + ": " + device.FriendlyName + " / " + device.State + " / " + device.ID);
|
|
112
|
+
if (device.State != DeviceState.Active) continue;
|
|
113
|
+
try { using (var client = device.AudioClient) lines.Add("Mix format: " + client.MixFormat + "; encoding " + client.MixFormat.Encoding); }
|
|
114
|
+
catch (Exception ex) { lines.Add("Mix format failed: " + ex); }
|
|
115
|
+
if (flow == DataFlow.Render) try { lines.Add("Muted: " + device.AudioEndpointVolume.Mute + "; volume: " + device.AudioEndpointVolume.MasterVolumeLevelScalar); } catch (Exception ex) { lines.Add("Volume query: " + ex.Message); }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (Exception ex) { lines.Add("Endpoint enumeration failed: " + ex); }
|
|
120
|
+
lines.Add(""); lines.Add("Available output choices:"); foreach (var choice in AudioOutputs.Choices()) lines.Add(choice.Name + " / " + choice.Key);
|
|
121
|
+
lines.Add(""); lines.Add("Available input choices:"); foreach (var choice in AudioInputs.Choices()) lines.Add(choice.Name + " / " + choice.Key);
|
|
122
|
+
if (File.Exists(AudioOutputs.ErrorLog)) try { lines.Add(""); lines.Add("Most recent failed/recovered output open:"); lines.Add(File.ReadAllText(AudioOutputs.ErrorLog)); } catch { }
|
|
123
|
+
return string.Join(Environment.NewLine, lines) + Environment.NewLine;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Collections.Generic;
|
|
5
|
+
using NAudio.CoreAudioApi;
|
|
6
|
+
using NAudio.Wave;
|
|
7
|
+
|
|
8
|
+
namespace DrumBuddy
|
|
9
|
+
{
|
|
10
|
+
public sealed class InputChoice
|
|
11
|
+
{
|
|
12
|
+
public string Name, Driver, EndpointId;
|
|
13
|
+
public string Key { get { return Driver != null ? "asio:" + Driver : "input:" + (EndpointId ?? "default"); } }
|
|
14
|
+
public override string ToString() { return Name; }
|
|
15
|
+
}
|
|
16
|
+
internal static class AudioInputs
|
|
17
|
+
{
|
|
18
|
+
public static List<InputChoice> Choices()
|
|
19
|
+
{
|
|
20
|
+
var choices = new List<InputChoice>();
|
|
21
|
+
try { foreach (string driver in AsioOut.GetDriverNames()) choices.Add(new InputChoice { Name = driver + " (ASIO)", Driver = driver }); } catch { }
|
|
22
|
+
if (WindowsWaveDevices.waveInGetNumDevs() > 0) choices.Add(new InputChoice { Name = "Windows default microphone - 16-bit PCM" });
|
|
23
|
+
try {
|
|
24
|
+
using (var enumerator = new MMDeviceEnumerator()) {
|
|
25
|
+
foreach (var endpoint in enumerator.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active)) {
|
|
26
|
+
using (endpoint) {
|
|
27
|
+
if (WindowsWaveDevices.Find(endpoint.ID, true) >= 0)
|
|
28
|
+
choices.Add(new InputChoice { Name = endpoint.FriendlyName + " (Windows microphone)", EndpointId = endpoint.ID });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch { }
|
|
33
|
+
return choices;
|
|
34
|
+
}
|
|
35
|
+
internal static int Device(InputChoice choice)
|
|
36
|
+
{
|
|
37
|
+
int device = string.IsNullOrEmpty(choice.EndpointId) ? -1 : WindowsWaveDevices.Find(choice.EndpointId, true);
|
|
38
|
+
if (device == -2) throw new IOException("The selected microphone is no longer available. Refresh Audio devices and select it again.");
|
|
39
|
+
return device;
|
|
40
|
+
}
|
|
41
|
+
internal static int Channels(InputChoice choice)
|
|
42
|
+
{
|
|
43
|
+
int device = Device(choice), channels = Math.Max(1, Math.Min(2, WindowsWaveDevices.InputCaps(device).Channels));
|
|
44
|
+
if (channels == 2 && !new[] { 44100, 48000, 96000 }.Any(rate => WindowsWaveDevices.SupportsInput(device, rate, 2))) channels = 1;
|
|
45
|
+
return channels;
|
|
46
|
+
}
|
|
47
|
+
internal static IEnumerable<int> Rates(InputChoice choice, int channels)
|
|
48
|
+
{ int device = Device(choice); return new[] { 44100, 48000, 96000 }.Where(rate => WindowsWaveDevices.SupportsInput(device, rate, channels)).ToArray(); }
|
|
49
|
+
}
|
|
50
|
+
}
|