-
-
Notifications
You must be signed in to change notification settings - Fork 138
/
Recorder.cs
527 lines (437 loc) · 18.1 KB
/
Recorder.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
using System;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EspionSpotify.API;
using EspionSpotify.AudioSessions;
using EspionSpotify.Enums;
using EspionSpotify.Exceptions;
using EspionSpotify.Extensions;
using EspionSpotify.Models;
using EspionSpotify.Native;
using EspionSpotify.Spotify;
using EspionSpotify.Translations;
using NAudio.Lame;
using NAudio.Wave;
namespace EspionSpotify
{
public sealed class Recorder : IRecorder, IDisposable
{
public const int MP3_MAX_NUMBER_CHANNELS = 2;
public const int MP3_MAX_SAMPLE_RATE = 48000;
private readonly bool _initiated;
private readonly FileManager _fileManager;
private readonly IFileSystem _fileSystem;
private readonly IFrmEspionSpotify _form;
private readonly Track _track;
private readonly IAudioThrottler _audioThrottler;
private readonly UserSettings _userSettings;
private bool _disposed;
private string _tempEncodeFile;
private string _tempOriginalFile;
private bool _canBeSkippedValidated;
private CancellationTokenSource _cancellationTokenSource;
private OutputFile _currentOutputFile;
private Stream _tempWaveWriter;
private IProcessManager _processManager;
public Recorder()
{
_track = new Track();
}
internal Recorder(
IFrmEspionSpotify form,
IAudioThrottler audioThrottler,
UserSettings userSettings,
ref Track track,
IFileSystem fileSystem) : this(form, audioThrottler, userSettings, ref track, fileSystem, new ProcessManager(), init: true) { }
public Recorder(
IFrmEspionSpotify form,
IAudioThrottler audioThrottler,
UserSettings userSettings,
ref Track track,
IFileSystem fileSystem,
IProcessManager processManager,
bool init)
{
_userSettings = new UserSettings();
userSettings.CopyAllTo(_userSettings);
_form = form;
_audioThrottler = audioThrottler;
_fileSystem = fileSystem;
_track = track;
_fileManager = new FileManager(_userSettings, _track, fileSystem);
_processManager = processManager;
_initiated = init && Init();
}
public Track Track => _track;
public bool IsSkipTrackActive =>
_userSettings.RecordRecordingsStatus == RecordRecordingsStatus.Skip
&& _fileManager.IsPathFileNameExists(_track, _userSettings, _fileSystem);
public int CountSeconds { get; set; }
public bool Running { get; set; }
private bool TrackIsFetchingMetadata => _track.MetaDataUpdated == null && !_userSettings.RecordEverythingEnabled && _userSettings.MediaFormat == MediaFormat.Mp3;
private WaveFormat WaveFormat => _audioThrottler.WaveFormat;
private bool Init()
{
_tempOriginalFile = _fileManager.GetTempFile();
try
{
_tempWaveWriter = new WaveFileWriter(_tempOriginalFile, WaveFormat);
}
catch (Exception ex)
{
ForceStopRecording();
_form.WriteIntoConsole(I18NKeys.LogUnknownException, ex.Message);
Console.WriteLine(ex.Message);
Program.ReportException(ex);
return false;
}
return true;
}
#region RecorderStart
public async Task Run(CancellationTokenSource cancellationTokenSource)
{
_cancellationTokenSource = cancellationTokenSource;
if (!_initiated || _userSettings.InternalOrderNumber > _userSettings.OrderNumberMax) return;
_form.WriteIntoConsole(I18NKeys.LogRecording, _track.ToString());
Running = true;
// await _audioThrottler.WaitBufferReady();
await RecordAvailableData(SilenceAnalyzer.TrimStart);
while (Running)
{
if (_cancellationTokenSource.IsCancellationRequested) return;
if (await StopRecordingIfTrackCanBeSkipped()) return;
await RecordAvailableData(SilenceAnalyzer.None);
}
await RecordAvailableData(SilenceAnalyzer.TrimEnd);
await RecordingStopped();
}
#endregion RecorderStart
#region RecorderWriteUpcomingData
private async Task RecordAvailableData(SilenceAnalyzer analyzer)
{
if (_tempWaveWriter == null) return;
var audio = await _audioThrottler.Read(analyzer);
if (audio == null) return;
await Task.Run(async () =>
{
await _tempWaveWriter.WriteAsync(
audio.Buffer,
0,
audio.BytesRecordedCount);
});
}
#endregion RecorderWriteUpcomingData
#region RecorderStopRecording
private async Task<bool> StopRecordingIfTrackCanBeSkipped()
{
if (_canBeSkippedValidated || TrackIsFetchingMetadata) return false;
_canBeSkippedValidated = true;
if (IsSkipTrackActive)
{
_form.WriteIntoConsole(I18NKeys.LogTrackExists, _track.ToString());
await UpdateMediaTagsWhenSkippingTrack();
ForceStopRecording();
if (_userSettings.ForceSpotifyToSkipEnabled)
{
var spotifyHandler = SpotifyProcess.GetMainSpotifyHandler(_processManager);
if (spotifyHandler.HasValue)
{
NativeMethods.SendKeyPessNextMedia(spotifyHandler.Value);
}
}
return true;
}
return false;
}
private async Task RecordingStopped()
{
while (TrackIsFetchingMetadata) await Task.Delay(100);
var skipped = !_canBeSkippedValidated && await StopRecordingIfTrackCanBeSkipped();
if (_tempWaveWriter == null || skipped)
{
ForceStopRecording();
return;
}
await _tempWaveWriter.FlushAsync();
var isTempWaveEmpty = _tempWaveWriter.Length == 0;
_tempWaveWriter.Dispose();
if (isTempWaveEmpty)
{
_form.WriteIntoConsole(I18NKeys.LogSpotifyPlayingOutsideOfSelectedAudioEndPoint);
ForceStopRecording();
return;
}
try
{
_tempEncodeFile = _fileManager.GetTempFile();
await WriteWaveFileToMediaFile();
}
catch (Exception ex)
{
_form.WriteIntoConsole(I18NKeys.LogUnknownException, ex.Message);
Console.WriteLine(ex.Message);
Program.ReportException(ex);
ForceStopRecording();
return;
}
_fileManager.DeleteFile(_tempOriginalFile);
_currentOutputFile = _fileManager.GetOutputFileAndInitDirectories();
if (CountSeconds < _userSettings.MinimumRecordedLengthSeconds)
{
_form.WriteIntoConsole(I18NKeys.LogDeleting, _currentOutputFile.ToString(),
_userSettings.MinimumRecordedLengthSeconds);
_fileManager.DeleteFile(_tempEncodeFile);
return;
}
try
{
_fileManager.RenameFile(_tempEncodeFile, _currentOutputFile.ToMediaFilePath());
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
ForceStopRecording();
if (ex is SourceFileNotFoundException)
{
_form.WriteIntoConsole(I18NKeys.LogRecordedFileNotFound);
}
else if (ex is DestinationPathNotFoundException)
{
_form.WriteIntoConsole(I18NKeys.LogOutputPathNotFound);
Watcher.Running = false;
}
else
{
_form.WriteIntoConsole(I18NKeys.LogException, ex.Message);
Program.ReportException(ex);
}
return;
}
var length = TimeSpan.FromSeconds(CountSeconds).ToString(@"mm\:ss");
_form.WriteIntoConsole(I18NKeys.LogRecorded, _currentOutputFile.ToString(), length);
await UpdateMediaTagsFileBasedOnMediaFormat();
EndRecording();
}
#endregion RecorderStopRecording
#region GetFileWriter
public Stream GetMediaFileWriter(Stream stream, WaveFormat waveFormat)
{
switch (_userSettings.MediaFormat)
{
case MediaFormat.Mp3:
var supportedWaveFormat = GetWaveFormatMP3Supported(waveFormat);
return new LameMP3FileWriter(stream, supportedWaveFormat, _userSettings.Bitrate);
case MediaFormat.Wav:
return new WaveFileWriter(stream, waveFormat);
default:
throw new Exception("Failed to get FileWriter");
}
}
#endregion GetFileWriter
#region TestFileWriter
public static bool TestFileWriter(IFrmEspionSpotify form, IMainAudioSession audioSession, UserSettings settings)
{
if (audioSession.AudioMMDevicesManager.AudioEndPointDevice == null) return false;
var waveIn = new WasapiLoopbackCapture(audioSession.AudioMMDevicesManager.AudioEndPointDevice);
switch (settings.MediaFormat)
{
case MediaFormat.Mp3:
try
{
using (new LameMP3FileWriter(new MemoryStream(), waveIn.WaveFormat, settings.Bitrate))
{
return true;
}
}
catch (ArgumentException ex)
{
return LogLameMP3FileWriterArgumentException(form, ex, waveIn.WaveFormat);
}
catch (Exception ex)
{
LogLameMP3FileWriterException(form, ex);
return false;
}
case MediaFormat.Wav:
try
{
using (new WaveFileWriter(new MemoryStream(), waveIn.WaveFormat))
{
return true;
}
}
catch (Exception ex)
{
form.UpdateIconSpotify(true, false);
form.WriteIntoConsole(I18NKeys.LogUnknownException, ex.Message);
Console.WriteLine(ex.Message);
Program.ReportException(ex);
return false;
}
default:
return false;
}
}
#endregion TestFileWriter
#region RecorderEncode
private async Task WriteWaveFileToMediaFile()
{
if (_userSettings.MediaFormat == MediaFormat.Wav)
// copy instead of moving to be able to keep using common code
_fileSystem.File.Copy(_tempOriginalFile, _tempEncodeFile);
else
await EncodeWaveFileToMediaFile();
}
private async Task EncodeWaveFileToMediaFile()
{
var restrictions = WaveFormat.GetMP3RestrictionCode();
using (var tempFileStream = _fileSystem.File.OpenRead(_tempOriginalFile))
{
tempFileStream.Position = 0;
using (var tempReader = new WaveFileReader(tempFileStream))
{
tempReader.Position = 0;
using (var mediaFileStream = _fileSystem.FileStream.Create(_tempEncodeFile, FileMode.Create,
FileAccess.ReadWrite, FileShare.ReadWrite))
{
using (var mediaWriter = GetMediaFileWriter(mediaFileStream, WaveFormat))
{
if (_userSettings.MediaFormat == MediaFormat.Mp3 && restrictions.Any())
await WriteWaveProviderReducerToMP3FileWriter(mediaWriter,
GetMp3WaveProvider(tempReader, WaveFormat));
else
await tempReader.CopyToAsync(mediaWriter, 81920, _cancellationTokenSource.Token);
}
}
}
}
}
#endregion RecorderEncode
#region RecorderUpdateMp3MataData
private async Task UpdateMediaTagsWhenSkippingTrack()
{
if (!_userSettings.UpdateRecordingsID3TagsEnabled) return;
_currentOutputFile = _fileManager.GetOutputFileAndInitDirectories();
await UpdateMediaTagsFileBasedOnMediaFormat();
}
private async Task UpdateMediaTagsFileBasedOnMediaFormat()
{
if (!_fileSystem.File.Exists(_currentOutputFile.ToMediaFilePath())) return;
switch (_userSettings.MediaFormat)
{
case MediaFormat.Mp3:
case MediaFormat.Wav:
var mapper = new MapperID3(
_currentOutputFile.ToMediaFilePath(),
_track,
_userSettings);
await Task.Run(mapper.SaveMediaTags);
return;
default:
return;
}
}
#endregion RecorderUpdateMp3MataData
#region MP3ConverterReducer
private static bool LogLameMP3FileWriterArgumentException(IFrmEspionSpotify form, ArgumentException ex,
WaveFormat waveFormat)
{
var restrictions = waveFormat.GetMP3RestrictionCode().ToList();
if (restrictions.Any())
{
if (restrictions.Contains(WaveFormatMP3Restriction.Channel))
form.WriteIntoConsole(I18NKeys.LogUnsupportedNumberChannels, waveFormat.Channels);
if (restrictions.Contains(WaveFormatMP3Restriction.SampleRate))
form.WriteIntoConsole(I18NKeys.LogUnsupportedRate, waveFormat.SampleRate);
return true;
}
form.UpdateIconSpotify(true);
form.WriteIntoConsole(I18NKeys.LogUnknownException, ex.Message);
return false;
}
private static void LogLameMP3FileWriterException(IFrmEspionSpotify form, Exception ex)
{
if (ex.Message.Contains("Unable to load DLL"))
{
form.WriteIntoConsole(I18NKeys.LogMissingDlls);
}
else
{
Program.ReportException(ex);
form.WriteIntoConsole(I18NKeys.LogUnknownException, ex.Message);
}
form.UpdateIconSpotify(true);
Console.WriteLine(ex.Message);
}
private WaveFormat GetWaveFormatMP3Supported(WaveFormat waveFormat)
{
return WaveFormat.CreateIeeeFloatWaveFormat(
Math.Min(MP3_MAX_SAMPLE_RATE, waveFormat.SampleRate),
Math.Min(MP3_MAX_NUMBER_CHANNELS, waveFormat.Channels));
}
private IWaveProvider GetWaveProviderMP3ChannelReducer(IWaveProvider stream)
{
var waveProvider = new MultiplexingWaveProvider(new[] {stream}, MP3_MAX_NUMBER_CHANNELS);
waveProvider.ConnectInputToOutput(0, 0);
waveProvider.ConnectInputToOutput(1, 1);
return waveProvider;
}
private IWaveProvider GetWaveProviderMP3SamplerReducer(IWaveProvider stream)
{
return new MediaFoundationResampler(stream, MP3_MAX_SAMPLE_RATE);
}
private async Task WriteWaveProviderReducerToMP3FileWriter(Stream mediaWriter, IWaveProvider stream)
{
var mp3WaveFormat = GetWaveFormatMP3Supported(WaveFormat);
var data = new byte[mp3WaveFormat.Channels * mp3WaveFormat.SampleRate * WaveFormat.Channels];
int bytesRead;
while ((bytesRead = stream.Read(data, 0, data.Length)) > 0)
await mediaWriter.WriteAsync(data, 0, bytesRead, _cancellationTokenSource.Token);
}
private IWaveProvider GetMp3WaveProvider(IWaveProvider stream, WaveFormat waveFormat)
{
var restrictions = waveFormat.GetMP3RestrictionCode().ToList();
if (restrictions.Contains(WaveFormatMP3Restriction.Channel))
stream = GetWaveProviderMP3ChannelReducer(stream);
if (restrictions.Contains(WaveFormatMP3Restriction.SampleRate))
stream = GetWaveProviderMP3SamplerReducer(stream);
return stream;
}
#endregion MP3ConverterReducer
#region DisposeRecorder
private void ForceStopRecording()
{
_form.UpdateIconSpotify(true);
Running = false;
EndRecording();
}
private void EndRecording()
{
TempWaveWriterDispose();
_fileManager.DeleteFile(_tempOriginalFile);
if (_currentOutputFile != null) _fileManager.DeleteFile(_tempEncodeFile);
}
private void TempWaveWriterDispose()
{
if (_tempWaveWriter == null) return;
_tempWaveWriter.Close();
_tempWaveWriter.Dispose();
_tempWaveWriter = null;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing) ForceStopRecording();
_disposed = true;
}
#endregion DisposeRecorder
}
}