-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathchronicle.cpp
More file actions
830 lines (698 loc) · 26.4 KB
/
chronicle.cpp
File metadata and controls
830 lines (698 loc) · 26.4 KB
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
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
/*
MIT License
Chronicle: an audio logger.
Copyright (c) 2016-2017 Callum McLean
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "chronicle.h"
enum AudioFormat
{
WAV,
OGG,
MP3,
FLAC
};
// Libsndfile stuff
SNDFILE *mySnd;
SF_INFO sfInfo;
// LAME stuff
lame_t lame_enc;
FILE *lameOutFile;
// The rest
AudioFormat destinationAudioFormat = AudioFormat::WAV;
std::string audioFileExtension = ".wav";
int sfSoundFormat = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
RtAudio audio;
bool do_record = false;
/* The data is cast to a short. sizeof(short) = 2 (bytes) = 16 bits.
Max value in a short is therefore 2^16 -1 = 65535.
However, since the values are read from -1 to +1, the range is halved to 32763.
It is appropriate to assume that a -40dB is sufficient for silence detection. Anything
lower than this is negligible, especially for analogue broadcast.
We can use this to figure out our silence detection.
Assuming 65535 is max volume (0dB).
I_db = 10 * log10(I / I_0)
-> I_db / 10 = log10(I / I_0)
-> 10^(I_db / 10) = I / I_0
-> I = 10^(I_db / 10) * I_0
-> I = 10^(-40 / 10) * 32763
-> I = 10^(-4) * 32763
-> I = 0.065535
*/
short maxAudioVal = (pow(2, (sizeof(short) * 8)) / 2) - 1;
constexpr int silenceThresholdDB = -40;
float thresholdVal = (pow(10, silenceThresholdDB / 10)) * maxAudioVal;
std::chrono::seconds audioFileAgeLimit = std::chrono::seconds(3628800);
unsigned int inputAudioDeviceId = audio.getDefaultInputDevice();
unsigned int inputAudioDeviceFirstChannel = 0;
unsigned int inputAudioDeviceChannelCount = 2;
unsigned int inputAudioDeviceSampleRate = 44100;
unsigned int outputStreamBitRate = 320;
boost::program_options::variables_map opts;
bool silent_flag = 0;
int main(int argc, char *argv[])
{
std::cout << SOFTWARE_NAME << " v" << SOFTWARE_VERSION_MAJOR << "." << SOFTWARE_VERSION_MINOR << "." << SOFTWARE_VERSION_PATCH << " Copyright (c) 2016-2019 Callum McLean" << std::endl
<< std::endl;
boost::filesystem::path output_directory;
std::string fileNameFormat = "%Y-%m-%d %H%M%S"; // Default strftime format for audio files. MinGW doesn't like %F...
/* Init logging */
try
{
boost::filesystem::create_directory("logs");
auto logger = spdlog::rotating_logger_mt("chronicle_log", "logs/chronicle.log", 1024 * 1024 * 5, 3);
logger->set_pattern("[%H:%M:%S %z] %l\t- %v");
logger->info("Chronicle started...");
}
catch (const spdlog::spdlog_ex &ex)
{
std::cout << "Cannot init logging: " << ex.what() << std::endl;
std::exit(1);
}
auto logger = spdlog::get("chronicle_log");
/* Parse cmd-line arguments */
{
opts = parse_cmd_opts(argc, argv);
{
// See if we're running in debug mode
if (opts.count("debug"))
{
logger->set_level(spdlog::level::debug);
logger->info("Log level: Debug");
logger->flush_on(spdlog::level::debug);
}
else
{
logger->set_level(spdlog::level::info);
logger->info("Log level: Info");
logger->flush_on(spdlog::level::info);
}
if (opts.count("list-devices"))
{
/* List the input devices that are available and exit. */
unsigned int devices = audio.getDeviceCount();
RtAudio::DeviceInfo deviceInfo;
if (devices < 1)
{
std::cout << "No devices found! Exiting..." << std::endl;
std::exit(0);
}
for (unsigned int i = 0; i < devices; i++)
{
deviceInfo = audio.getDeviceInfo(i);
if (deviceInfo.probed == true && deviceInfo.inputChannels != 0)
{
std::cout << "#" << i << ": " << deviceInfo.name;
if (deviceInfo.isDefaultInput)
{
std::cout << " (default)";
}
std::cout << std::endl;
std::cout << " Channel count: " << deviceInfo.inputChannels << std::endl;
}
}
deviceInfo = audio.getDeviceInfo(audio.getDefaultInputDevice());
std::cout << std::endl
<< "Default device: " << deviceInfo.name << std::endl;
std::exit(0);
}
/* Check for conflicting options */
if (opts.count("no-delete") & opts.count("max-age") & !opts["max-age"].defaulted())
{
printf("Cannot supply --no-delete and --max-age together; they are incompatible\n");
std::exit(1);
}
/* Validate and set recording options */
if (opts.count("directory"))
{
boost::filesystem::path proposedDir = opts["directory"].as<std::string>();
output_directory = proposedDir;
}
if (opts.count("format"))
{
fileNameFormat = opts["format"].as<std::string>();
/* strftime seems to not produce anything when passed %F on MinGW-compiled versions,
Fixes #26 */
size_t found = fileNameFormat.find("%F");
while (found != std::string::npos)
{
fileNameFormat.replace(found, 2, "%Y-%m-%d");
found = fileNameFormat.find("%F");
}
}
if (opts.count("max-age"))
{
std::string max_age_string = opts["max-age"].as<std::string>();
int max_age_val;
std::string max_age_unit;
try
{
max_age_val = stoi(max_age_string);
logger->debug("Found max age value: {}", max_age_val);
}
catch (std::invalid_argument e)
{
printf("Cannot identify a valid duration for --max-age (supplied value: %s )\n. See chronicle --help for more details.\n", max_age_string.c_str());
std::exit(1);
}
int unit_str_idx = max_age_string.find_first_of("smhdSMHD");
if (unit_str_idx == std::string::npos)
{
printf("Cannot identify a valid duration for --max-age (supplied value: %s )\n. See chronicle --help for more details.\n", max_age_string.c_str());
std::exit(1);
}
max_age_unit = max_age_string.substr(unit_str_idx, 1);
logger->debug("Found max age unit: {}", max_age_unit);
/* Allow the user to specify age limit in units other than seconds,
Fixes #12 */
std::chrono::seconds age_seconds;
if (max_age_unit == "s" || max_age_unit == "S")
{
age_seconds = std::chrono::seconds(max_age_val);
}
if (max_age_unit == "m" || max_age_unit == "M")
{
age_seconds = std::chrono::minutes(max_age_val);
}
if (max_age_unit == "h" || max_age_unit == "H")
{
age_seconds = std::chrono::hours(max_age_val);
}
if (max_age_unit == "d" || max_age_unit == "D")
{
age_seconds = std::chrono::hours(max_age_val * 24);
}
if (max_age_val == 1 && (max_age_unit == "s" || max_age_unit == "S"))
{
std::cout << "The specified file age limit must be greater than 1 second:";
std::cout << max_age_val << std::endl;
std::exit(1);
}
audioFileAgeLimit = age_seconds;
}
if (opts.count("audio-format"))
{
std::string audio_format_string = opts["audio-format"].as<std::string>();
logger->debug("Found proposed audio format: {}", audio_format_string);
// if (opts.audio_format == "OGG" || opts.audio_format == "ogg")
if (audio_format_string == "OGG" || audio_format_string == "ogg")
{
sfSoundFormat = SF_FORMAT_OGG | SF_FORMAT_VORBIS;
audioFileExtension = ".ogg";
destinationAudioFormat = AudioFormat::OGG;
}
else if (audio_format_string == "FLAC" || audio_format_string == "flac")
{
sfSoundFormat = SF_FORMAT_FLAC | SF_FORMAT_PCM_16;
audioFileExtension = ".flac";
destinationAudioFormat = AudioFormat::FLAC;
}
else if (audio_format_string == "WAV" || audio_format_string == "wav")
{
sfSoundFormat = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
audioFileExtension = ".wav";
destinationAudioFormat = AudioFormat::WAV;
}
else if (audio_format_string == "MP3" || audio_format_string == "mp3")
{
audioFileExtension = ".mp3";
destinationAudioFormat = AudioFormat::MP3;
}
else
{
printf("Supplied audio file format not supported: %s\n", audio_format_string.c_str());
printf("Supported formats are: [ OGG | WAV | MP3 | FLAC ]\n");
std::exit(1);
}
logger->info("\tUsing audio format: {}", audio_format_string);
}
// if (opts.input_device != -1)
if (opts.count("input-device"))
{
RtAudio::DeviceInfo proposedDeviceInfo;
unsigned int input_device_id = opts["input-device"].as<unsigned int>();
logger->debug("Found proposed input device ID: {}", input_device_id);
/* Does a device exist with the provided ID? */
if (input_device_id > (audio.getDeviceCount() - 1))
{
printf("No audio device found with ID %ul\n", input_device_id);
printf("Use chronicle --list-devices to find a list of available device IDs.\n");
std::exit(1);
}
/* Is the device an input device? */
logger->debug("Getting info for device ID {}", input_device_id);
proposedDeviceInfo = audio.getDeviceInfo(input_device_id);
if (proposedDeviceInfo.inputChannels == 0)
{
printf("The specified audio device is not an input device: %ul : %s\n", input_device_id, proposedDeviceInfo.name.c_str());
std::exit(1);
}
inputAudioDeviceId = input_device_id;
}
if (opts.count("device-first-channel"))
{
unsigned int proposed_first_channel = opts["device-first-channel"].as<unsigned int>();
logger->debug("Found proposed device first channel: {}", proposed_first_channel);
RtAudio::DeviceInfo device_info = audio.getDeviceInfo(inputAudioDeviceId);
logger->debug("\t{} input channels available, {} requested as first", device_info.inputChannels, proposed_first_channel);
if (device_info.inputChannels - 1 < proposed_first_channel)
{
printf("Option device-first-channel has been specified as %i, but device %i (%s) only has %i input channels\n",
proposed_first_channel, inputAudioDeviceId, device_info.name.c_str(), device_info.inputChannels);
std::exit(1);
}
inputAudioDeviceFirstChannel = proposed_first_channel;
}
if (opts.count("device-channels"))
{
unsigned int proposed_device_channels = opts["device-channels"].as<unsigned int>();
logger->debug("Found proposed device channel count: {}", proposed_device_channels);
if (proposed_device_channels < 1)
{
printf("Invalid device channel count: %i", proposed_device_channels);
std::exit(1);
}
RtAudio::DeviceInfo device_info = audio.getDeviceInfo(inputAudioDeviceId);
logger->debug("\t{} input channels available, {}-{} requested for use", device_info.inputChannels, inputAudioDeviceFirstChannel, proposed_device_channels + inputAudioDeviceFirstChannel - 1);
if (inputAudioDeviceFirstChannel + proposed_device_channels > device_info.inputChannels)
{
printf("Option device-channels has been specified as %i, but device %i (%s) only has %i input channels (first channel: %i)\n",
proposed_device_channels, inputAudioDeviceId, device_info.name.c_str(), device_info.inputChannels, inputAudioDeviceFirstChannel);
std::exit(1);
}
inputAudioDeviceChannelCount = proposed_device_channels;
}
if (opts["device-channels"].as<unsigned int>() != 2 && destinationAudioFormat == AudioFormat::MP3)
{
printf("MP3 recording format only supports using 2 channels.\n");
std::exit(1);
}
if (opts.count("sample-rate"))
{
unsigned int proposed_sample_rate = opts["sample-rate"].as<unsigned int>();
logger->debug("Found proposed sample rate: {}", proposed_sample_rate);
RtAudio::DeviceInfo device_info = audio.getDeviceInfo(inputAudioDeviceId);
unsigned int matching_rate = 0;
for (auto it : device_info.sampleRates)
{
logger->debug("\ttesting supported sample rate {}", it);
if (it == proposed_sample_rate)
{
matching_rate = proposed_sample_rate;
logger->debug("\tsuccess!");
break;
}
}
if (!matching_rate)
{
printf("Device %i (%s) does not support specified sample rate: %i\n", inputAudioDeviceId,
device_info.name.c_str(), proposed_sample_rate);
std::exit(1);
}
inputAudioDeviceSampleRate = matching_rate;
}
if (opts.count("bitrate"))
{
if (destinationAudioFormat != AudioFormat::MP3)
{
printf("Audio format is not MP3, bitrate option will be ignored.");
}
outputStreamBitRate = opts["bitrate"].as<unsigned int>();
}
if (opts.count("no-term"))
{
NC_UI_IS_ENABLED = false;
}
else
{
NC_UI_IS_ENABLED = true;
}
}
}
/* Make sure that audio devices exist before continuing... */
if (audio.getDeviceCount() < 1)
{
logger->critical("No audio devices available! Exiting...");
std::exit(0);
}
logger->debug("Output directory: " + output_directory.string());
std::string windowTitle = "Chronicle v" + SOFTWARE_VERSION_MAJOR + "." + SOFTWARE_VERSION_MINOR + "." + SOFTWARE_VERSION_PATCH;
if (NC_UI_IS_ENABLED)
{
initCurses(windowTitle);
setSigIntCallback(signalShutdownHandler);
}
do_record = true;
doRecord(output_directory, fileNameFormat);
if (NC_UI_IS_ENABLED)
{
closeCurses();
}
return 0;
}
void doRecord(boost::filesystem::path directory, std::string fileNameFormat)
{
RtAudio::StreamParameters params;
RtAudio::DeviceInfo deviceInfo = audio.getDeviceInfo(inputAudioDeviceId);
auto logger = spdlog::get("chronicle_log");
logger->info("Using input device: {}", deviceInfo.name);
recordingParameters rp = getRecordingParameters(deviceInfo);
params.deviceId = inputAudioDeviceId;
params.nChannels = rp.channelCount;
params.firstChannel = rp.firstChannel;
// Generate libsndfile information if we're using it
if (destinationAudioFormat == FLAC || destinationAudioFormat == OGG || destinationAudioFormat == WAV)
{
sfInfo.channels = rp.channelCount;
sfInfo.format = sfSoundFormat;
sfInfo.samplerate = rp.sampleRate;
if (sf_format_check(&sfInfo) == 0)
{
logger->critical("Destination format invalid, exiting...");
std::exit(0);
}
sf_command(mySnd, SFC_SET_SCALE_FLOAT_INT_READ, NULL, SF_TRUE);
}
// Or setup the MP3 encoder
else if (destinationAudioFormat == MP3)
{
lame_enc = lame_init();
lame_set_in_samplerate(lame_enc, rp.sampleRate);
lame_set_out_samplerate(lame_enc, rp.sampleRate);
lame_set_VBR(lame_enc, vbr_abr);
lame_set_VBR_mean_bitrate_kbps(lame_enc, outputStreamBitRate);
lame_set_num_channels(lame_enc, rp.channelCount);
int ret = lame_init_params(lame_enc);
if (ret < 0)
{
logger->critical("Error occurred when initializing LAME parameters. Code: {}", ret);
}
}
updateAudioDevice(deviceInfo.name, rp.sampleRate, rp.channelCount);
// Set up signal handling; fixes #1
{
signal(SIGINT, signalShutdownHandler);
signal(SIGABRT, signalShutdownHandler);
#ifndef _WIN32
signal(SIGWINCH, signalWinResizeHandler);
#endif
//signal(SIGBREAK, signalHandler);
}
// Main record loop begins
while (do_record)
{
/* The time to finish recording is at the end of the hour.
We can't assume that the recording starting from the top of the current hour,
as the first recording probably won't be.
So, we'll add an hour to the current time, and then remove any minutes and
seconds from that time.
*/
std::chrono::time_point<std::chrono::system_clock> endTime = calculateRecordEndTimeFromNow();
char audioFileName[81];
time_t now_tt = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
struct tm now_tm;
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__)
localtime_s(&now_tm, &now_tt); // Use localtime_s on windows
#else
localtime_r(&now_tt, &now_tm); // Use localtime_r on POSIX
#endif
strftime(audioFileName, 80, fileNameFormat.c_str(), &now_tm);
boost::filesystem::path audioFileFullPath;
audioFileFullPath = directory;
audioFileFullPath /= audioFileName;
audioFileFullPath.replace_extension(audioFileExtension);
/* If the directory so far does not exist - create it.
This is helpful if supplying directory arguments as part of --format;
for example, -f "%Y/%M/D/%H-%m-%s.wav" would produce
'2017/03/13/16-00-00.wav'.
Fixes #23 */
try
{
boost::filesystem::create_directories(audioFileFullPath.parent_path());
}
catch (boost::filesystem::filesystem_error &e)
{
printf("%s", e.what());
logger->critical("Could not create directory: {}", e.what());
std::exit(1);
}
catch (std::exception &e)
{
logger->critical("Could not create directory: {}", e.what());
}
/* Calculate the file size - fixes #22 */
std::chrono::time_point<std::chrono::system_clock> tpNow = std::chrono::system_clock::now();
std::chrono::seconds recordDuration = std::chrono::duration_cast<std::chrono::seconds>(endTime - tpNow);
long fileSizeMB = calculateHardDriveUsage(recordDuration, rp);
boost::filesystem::space_info diskSpace = boost::filesystem::space(directory);
long diskSpaceAvailableGB = diskSpace.available / 1073741824; // bytes to GB
updateHardDriveSpace(diskSpaceAvailableGB, fileSizeMB);
// Maintenance
if (!opts.count("no-delete"))
{
removeOldAudioFiles(audioFileAgeLimit, directory);
}
// Open audio file for writing
if (destinationAudioFormat != MP3)
{
mySnd = sf_open(audioFileFullPath.generic_string().c_str(), SFM_WRITE, &sfInfo);
/* Check if the file can be opened. Fixes #6. */
if (mySnd == NULL)
{
// Can't open the file. Exit.
logger->critical("Could not start recording: {}", sf_strerror(mySnd));
std::exit(1);
}
}
else
{
lameOutFile = fopen(audioFileFullPath.generic_string().c_str(), "wb+");
if (lameOutFile == NULL)
{
// Can't open the file. Exit.
logger->critical("Couldn't open the destination file. Error: {}", strerror(errno));
std::exit(1);
}
}
try
{
updateRecordingToPath(audioFileFullPath.generic_string());
logger->debug("Updated recording output path: {}", audioFileFullPath.generic_string());
audio.openStream(NULL, ¶ms, RTAUDIO_SINT16, rp.sampleRate, &(rp.bufferLength), &cb_record, &(rp.channelCount), NULL, &onRtAudioError);
logger->debug("Opened audio stream");
audio.startStream();
logger->info("Started new recording");
}
catch (RtAudioError &e)
{
logger->critical("Could not open stream: {}", e.getMessage());
std::exit(0);
}
catch (std::exception &e)
{
logger->critical("Could not begin recording: {}", e.what());
}
while (do_record && std::chrono::system_clock::now() <= endTime)
{
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
logger->info("Recording completed");
stopRecord();
}
}
int cb_record(void *outputBuffer, void *inputBuffer, unsigned int nFrames, double streamTime, RtAudioStreamStatus status, void *userData)
{
/* userData is the channel count. */
int *pChannelCount = (int *)userData;
int channelCount = *pChannelCount;
short *data = (short *)inputBuffer;
if (destinationAudioFormat == WAV || destinationAudioFormat == OGG || destinationAudioFormat == FLAC)
{
sf_writef_short(mySnd, data, nFrames);
}
else if (destinationAudioFormat == MP3)
{
unsigned char MP3Buffer[8192];
int enc_data_size = lame_encode_buffer_interleaved(lame_enc, data, nFrames, MP3Buffer, 8192);
fwrite(MP3Buffer, enc_data_size, 1, lameOutFile);
}
/* To figure out if a buffer is silent, we need to check the content of the buffer.
The buffer is just the (number of frames) * (the number of channels); in our case,
nFrames*numChannels. Each buffer is then just a number, representing the amplitude of the audio.
Therefore, if the buffer is empty, the audio is silent. */
// Do this for each channel
for (int ch = 0; ch < channelCount; ch++)
{
short framesPeak = 0;
// Out of all of the frames we have available, iterate over the ones for the given channel and find the loudest
// It's interleaved audio (i.e. LRLRLRLRLR), so we skip every channelCount'th frame
for (int i = ch; i < nFrames * channelCount; i += channelCount)
{
short val = abs(*(data + i));
framesPeak = std::max(val, framesPeak);
};
// I_db = 10*log10(I/I_0)
// Substitute 1 for framesPeak if it's actually 0, or the maths doesn't work. We can live we the inaccuracy at this low level.
float level = 10 * (log10(float((framesPeak > 0) ? framesPeak : 1) / float(maxAudioVal)));
char label[10];
// If framesPeak < 0, print the level as being "-INF db"
(framesPeak > 1) ? sprintf(label, "%02.2f dB", level) : sprintf(label, " -INF dB");
// TODO: Rather than calling this for every channel individually it would be nicer to just pass an array of values representing all of the channels...
updateAudioMeter(ch, abs(silenceThresholdDB), abs(silenceThresholdDB) - abs(level), label);
}
return 0;
}
void stopRecord()
{
auto logger = spdlog::get("chronicle_log");
try
{
audio.stopStream();
}
catch (RtAudioError &e)
{
logger->warn("Could not stop stream: {}", e.getMessage());
}
if (audio.isStreamOpen())
{
audio.closeStream();
}
if (destinationAudioFormat != MP3)
{
sf_write_sync(mySnd);
sf_close(mySnd);
}
else
{
unsigned char discarded_buffer[8192];
int remaining_frames = lame_encode_flush(lame_enc, discarded_buffer, 8192);
logger->debug("Flushed LAME");
lame_mp3_tags_fid(lame_enc, lameOutFile);
fclose(lameOutFile);
logger->debug("Closed destination file");
// logger->debug("Cleared LAME encoder");
}
}
float calculateHardDriveUsage(std::chrono::seconds duration, recordingParameters rp)
{
/* WAV file size:
sz_in_mb = (bit_depth * sample_rate * channels * dur_in_secs) / 8 / 1000000
*/
if (destinationAudioFormat == WAV || destinationAudioFormat == FLAC)
{
return (rp.sampleRate * 16.00 * rp.channelCount * duration.count()) / 8 / 1024 / 1024;
}
else if (destinationAudioFormat == OGG)
{
// The OGG encoding uses VBR with a mean bit rate of 128 kbps
return (128 * duration.count() / 8 / 1024);
}
else if (destinationAudioFormat == MP3)
{
// Our MP3 encoding uses VBR, at 320kbps
return (outputStreamBitRate * duration.count() / 8 / 1024);
}
else
{
// Something's gone wrong...
return 0;
}
}
recordingParameters getRecordingParameters(RtAudio::DeviceInfo recordingDevice)
{
auto logger = spdlog::get("chronicle_log");
recordingParameters rp;
// (recordingDevice.inputChannels == 1) ? rp.channelCount = 1 : rp.channelCount = 2;
rp.channelCount = inputAudioDeviceChannelCount;
rp.firstChannel = inputAudioDeviceFirstChannel;
logger->debug("Input device has {} channels, using {}", recordingDevice.inputChannels, rp.channelCount);
rp.sampleRate = inputAudioDeviceSampleRate;
logger->debug("Using sample rate: {}", rp.sampleRate);
rp.bufferLength = 1024;
return rp;
}
std::chrono::time_point<std::chrono::system_clock> calculateRecordEndTimeFromNow()
{
std::chrono::time_point<std::chrono::system_clock> nowChrono, endChronoInaccurate, endChronoAccurate;
nowChrono = std::chrono::system_clock::now();
endChronoInaccurate = nowChrono + std::chrono::seconds(3600);
// Convert the end-time into a form than we can manipulate
time_t end_tt = std::chrono::system_clock::to_time_t(endChronoInaccurate);
struct tm *end_tm = localtime(&end_tt);
// And remove any extraneous hours/minutes so the time is at the top of hour
end_tm->tm_min = 0;
end_tm->tm_sec = 0;
// Now convert back to a chrono
end_tt = mktime(end_tm);
endChronoAccurate = std::chrono::system_clock::from_time_t(end_tt);
return endChronoAccurate;
}
void removeOldAudioFiles(std::chrono::seconds age, boost::filesystem::path directory)
{
/* Iterate over the files in a directory (presumably the output directory) and delete
audio files older than a certain age
*/
auto logger = spdlog::get("chronicle_log");
logger->debug("Removing old audio files (max age is {} seconds)", audioFileAgeLimit.count());
std::chrono::system_clock::time_point nowChrono, oldestTimeChrono, fileMTime;
nowChrono = std::chrono::system_clock::now();
oldestTimeChrono = nowChrono - age;
boost::filesystem::directory_iterator dirIterEnd = boost::filesystem::directory_iterator();
boost::filesystem::directory_iterator dirIter = boost::filesystem::directory_iterator(directory);
while (dirIter != dirIterEnd)
{
boost::filesystem::directory_entry dirEntry;
dirEntry = *dirIter;
std::chrono::system_clock::time_point fileMTime = std::chrono::system_clock::from_time_t(boost::filesystem::last_write_time(dirEntry.path()));
if ((fileMTime < oldestTimeChrono) & (dirEntry.path().extension() == audioFileExtension))
{
logger->debug("\tDeleting file older than max age (age is {}s): {}", std::chrono::duration_cast<std::chrono::seconds>(nowChrono - fileMTime).count(), dirEntry.path().string());
boost::filesystem::remove(dirEntry.path());
}
dirIter++;
}
}
void signalWinResizeHandler(int sigNum)
{
auto logger = spdlog::get("chronicle_log");
logger->info("Handling window resize");
onWindowResize();
}
void signalShutdownHandler(int sigNum)
{
auto logger = spdlog::get("chronicle_log");
logger->info("Received signal {}; shutting down...", sigNum);
do_record = false;
stopRecord();
closeCurses();
std::exit(sigNum);
}
void onRtAudioError(RtAudioError::Type type, const std::string &errorText)
{
auto logger = spdlog::get("chronicle_log");
switch (type)
{
case RtAudioError::Type::DEBUG_WARNING:
case RtAudioError::Type::WARNING:
logger->warn("Got RtAudio warning: {}", errorText);
break;
default:
logger->error("Got RtAudio error: {}", errorText);
signalShutdownHandler(1);
break;
}
}