Introduction
Here is an app for recording from the sound card in MP3 format. This utility adds a user interface to a console application, mp3_stream.exe, described by rtybase in an article which appeared here on CodeProject a while ago. It also adds new features, such as setting a recording time duration to automatically end a lengthy recording session. Other features include saving current user settings on exit, and reloading those settings the next time the tool is launched.
From a technical point of view, the article describes how a console third-party application can be controlled by spawning it into a separate process, and communicating with it through command-line arguments and callback methods, while making use of async/await to keep the user interface smooth and responsive.
Background
I had put this utility together fairly quickly as a replacement for some DOS batch files which I used to drive the mp3_stream.exe with. I then soon realised that it would be useful to add a duration timer so that a recording can be ended whilst the computer is left unattended. I found this to be particularly useful when recording longer sessions, some of which can be over six hours long.
Using the Utility
The dialog shows the settings which represent the command line arguments for mp3_stream.exe, such as the volume, bit rate (in kilo bits per second) e.g. 128. The Device and line names are shown in dropdown list boxes. The total duration of the recording can be optionally specified, and a file name which the recording is to be saved as can be browsed for using the standard Windows Save File As dialog. All configuration settings are saved on closing down the utility, and are automatically reloaded the next time it is launched.
Windows 7, 8, 8.1, and 10 Usability Notes
Since I first published this article, more and more people started using it in post-Windows XP operating systems, naturally. The issue encountered when using later versions of Windows is that the operating system both disables and hides the Stereo Mix device by default, even if it is available through the sound card's driver. Overcoming this issue is fairly straightforward, as follows:
- Right-click on the speaker icon in the system tray and select Recording devices (you can also bring up this dialog through Windows control panel)
- Right-click inside the dialog to bring up the context menu, and select Show Disabled Devices
- Again, right-click inside the dialog to bring up the context menu, and this time select Show Disconnected Devices
- The above two steps will make Stereo Mix visible, right-click on it and select Enable
- Close the dialog by pressing the OK button
Now the next time you launch the utility, you will be able to select the Stereo Mix device and Master Volume line.
It is also worth mentioning that the sound card's driver, as issued by the manufacturer (e.g. Realtek) may not always be installed by default on a newly purchased machine. In many cases, only the standard generic Windows driver is installed. This can be overcome easily by downloading the manufacturer's driver and installing it on the machine. It is then possible to carry out steps 1 to 5 above, and enjoy the benefits of recording from the sound card.
How the Code Works
mp3_stream.exe itself is a C++ application, which uses the LAME open source library to carry out the MP3 encoding. To communicate to LAME using a .NET application, one could go about this in several ways. For example, one could add a thin interface layer of managed C++ to the mp3_stream.exe source code and recompile it so that it can be accessed via .NET. Or, one could add a thin interface layer on the C# project side (using [DllImport]
) to access the LAME DLL functionality directly.
However, mp3_stream.exe does in fact already provide an API which is perfectly accessible from .NET. This is perhaps not what one would normally think of as an API in a conventional sense, because it is simply the command line arguments supported by mp3_stream.exe. Nevertheless, it is a programmable interface which provides access to the desired functionality perfectly well. For example, to enumerate the sound cards supported by the system, mp3_stream.exe is invoked with a -device
argument. To do this programmatically, System.Diagnostics.Process
is used to spawn a mp3_stream.exe process providing it with the appropriate arguments. This is done by the Execute
method, which in turn calls the InitiateMP3StreamProcess
method:
private static Process InitiateMp3StreamProcess(string arguments, EventHandler onExecutionCompleted)
{
var recordingProc = new Process
{
StartInfo =
{
CreateNoWindow = true,
WorkingDirectory = Application.StartupPath,
FileName = "mp3_stream.exe",
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
},
EnableRaisingEvents = true
};
if (onExecutionCompleted != null)
recordingProc.Exited += onExecutionCompleted;
recordingProc.Start();
return recordingProc;
}
private static IEnumerable<string> Execute(string command)
{
var process = InitiateMp3StreamProcess(command, null);
process.WaitForExit();
return ReadResponseLines(process);
}</string>
The onExecutionCompleted argument can be used to supply a callback event which is called when the external process has completed and exited. This callback is useful for more lengthy operations, such as when performing a recording, and can be seen in action in the Recording section below.
Other operations, however, are practically instantaneous and would not block the user interface for any appreciable period of time. For such operations, there is no need to supply a callback, and we can simply use Process.WaitForExit() to wait the few milliseconds it would take for the process to complete. This type of interaction can be thought of as direct execution, and can be invoked via the Execute
method.
The method Execute
is called with a string
which specified the command-line argument which needs to be passed to the mp3_stream.exe process:
Execute("-devices");
This is used, in conjunction with using the -device
argument, to iteratively populate the DeviceLines
dictionary with all the available devices and lines:
private void PopulateDevices()
{
var devices = Execute("-devices");
foreach (var device in devices)
{
Devices.Items.Add(device);
DeviceLines.Add(device, new List<string>());
var lines = Execute($"-device=\"{device}\"");
foreach (var line in lines)
{
DeviceLines[device].Add(line);
}
}
}</string>
Recording
Once configured and the Record button is pressed, the utility displays the time left for the recording to complete. This is done using a timer, which updates a progress bar and a textual display of time remaining:
private void UpdateTimeRemaining(TimeSpan timeSpan)
{
TimeRemaining.Text =
@"Time remaining: " +
$"{timeSpan.Hours.ToString().PadLeft(2, '0')}:"+
$"{timeSpan.Minutes.ToString().PadLeft(2, '0')}:"+
$"{timeSpan.Seconds.ToString().PadLeft(2, '0')}";
}
private void Timer_Tick(object sender, EventArgs e)
{
var timeNow = DateTime.Now;
if (timeNow >= EndTime)
{
OnStopRecording();
}
else
{
UpdateProgressIndicators(timeNow);
}
}
private void UpdateProgressIndicators(DateTime timeNow)
{
var timeRemaining = EndTime - timeNow;
UpdateTimeRemaining(timeRemaining);
var timeElapsed = timeNow - StartTime;
var totalDuration = EndTime - StartTime;
var percentageElapsed = (timeElapsed.TotalMilliseconds/totalDuration.TotalMilliseconds)*100;
if (percentageElapsed > 0)
{
Progress.Value = (int) percentageElapsed;
}
else
{
Progress.Value = 0;
}
}
The recording operation itself is invoked by the Click
event handler of the Record button:
private async void StartRecording_Click(object sender, EventArgs e)
{
StartRecording();
await RecordingCompletedSignal.WaitAsync();
StopRecording();
}
You may have noticed StartRecording_Click
and wondered why it is implemented as an async
method. And you may have also wondered about the merits of making a method which returns void
async, not least given the received wisdom to avoid async void. This is not really as much of a concern as it may first appear, the only exception to the rule of avoiding async void
is when the method in question is an event handler, which is precisely what we are dealing with here. StartRecording_Click
is an event handler, which means it being a void
method is perhaps not a surprise, and turning it into an async method is absolutely fine.
The StartRecording()
method makes use of the optional callback function, supplied as the second argument to the InitiateMP3StreamProcess
method, in the form of an anonymous method which in turn calls RecordingCompletedSignal.Release()
. The sequence of events goes as follows: the recording is deemed to have completed when the recording duration time has expired, or when the user manually clicks the Stop button. In either case, the external process is sent a quit message, which allows it to complete the recording and shutdown gracefully, and also call-back RecordingCompletedSignal.Release()
on its exit. What this does is, it fires the RecordingCompletedSignal
semaphore, on which we await asynchronously using WaitAsync()
.
private void StartRecording()
{
var configuration =
$"-device=\"{Devices.SelectedItem}\" " +
$"-line=\"{Lines.SelectedItem}\" " +
$"-v={Volume.Text} " +
$"-br={BitRate.Text} -sr=32000";
RecordingProcess =
InitiateMp3StreamProcess(configuration, (s, e) => RecordingCompletedSignal.Release());
StartRecordingButton.Enabled = false;
StopRecordingButton.Enabled = true;
UseTimedRecordingIfNecessary();
}
The alternative to using this coordinated asynchronous machinery would be to call Process.Kill()
directly instead, which is in fact how the initial versions of this tool operated. This presented some issues which needed to be worked around. For example, I found that using Process.Kill()
caused the total time of recordings made using mp3_stream.exe to be always shorter than expected by around 3 seconds, which I needed to allow for. This might have been a consequence of the encoding process throwing away a small chunk of data at the end of its buffer, on being abruptly killed off.
private void StopRecording()
{
RecordingProcess.WaitForExit();
RecordingProcess.Close();
SaveRecording();
UpdateTimeRemaining(new TimeSpan(0, 0, 0));
Progress.Style = ProgressBarStyle.Continuous;
Progress.Value = 100;
StartRecordingButton.Enabled = true;
StopRecordingButton.Enabled = false;
}
It is not possible to await a callback method directly, only a Task
can be truly awaited. To achieve this, the callback function uses RecordingCompletedSignal
through an anonymous method (s, e) => RecordingCompletedSignal.Release()
which is invoked on the completion of the mp3_stream.exe process. This means StartRecording_Click
can now await the Task
object returned by SemaphoreSlim.WaitAsync()
.
To make this possible, there were some complications to overcome in the mp3_stream external application itself, in that it was polling the console input for the quit signal, in a "please press any key to quit" style. The issue here is that it is not possible to redirect a console input, only the standard input can be redirected as such. To send the quit key press to the external application would have required obtaining an unsafe handle and directly copying the key char to its buffer. I was able to avoid all these complications by simply recompiling mp3_stream after making a minor modification which replaced the _kbhit()
call with reading the standard input instead, using cin >>
.
By updating the code to make use of async methods, callback functions, semaphores, and so on as described above, it now is possible to end the mp3_stream.exe more gracefully, and await a callback to receive notification of its completion.
Saving and Retrieving the Configuration
For added convenience, the tool automatically saves its current configuration on exit. This configuration is then reloaded the next time the tool is launched. This is achieved by handling the form's FormClosing
and Load
events:
private void Record_FormClosing(object sender, FormClosingEventArgs e)
{
SaveCurrentConfiguration();
}
private void Record_Load(object sender, EventArgs e)
{
RestorePreviousConfiguration();
}
The SaveCurrentConfiguration
and RestorePreviousConfiguration
methods in turn use the Settings
class to access the application configuration parameters:
private void RestorePreviousConfiguration()
{
var config = Settings.Default;
Volume.Text = config.Volume;
BitRate.Text = config.BitRate;
FileName.Text = config.FileName;
if (Devices.Items.Contains(config.Device))
{
Devices.SelectedItem = config.Device;
}
if (Lines.Items.Contains(config.Line))
{
Lines.SelectedItem = config.Line;
}
}
private void SaveCurrentConfiguration()
{
var config = Settings.Default;
config.Volume = Volume.Text;
config.BitRate = BitRate.Text;
config.FileName = FileName.Text;
if (Devices.SelectedItem != null)
{
config.Device = Devices.SelectedItem.ToString();
}
if (Lines.SelectedItem != null)
{
config.Line = Lines.SelectedItem.ToString();
}
config.Save();
}
Feature Requests
A few people got in touch, since this article was first published, and asked if the utility could be enhanced to add support for new features. I will add here new subsections each time a new feature or enhancement has been incorporated. Please feel free to keep your requests coming. I will endeavour to accommodate as many requests as possible, just as soon as I can.
Auto File Names
A popular request was to add an enhancement so that the same file name can be used more than once. A common way of using the utility, it appears, is to keep the utility up and running and perform multiple recordings in succession. Having to keep specifying a new file name, in between recordings, breaks the flow of this usage pattern. And so, many enhancement requests were centred around how to make this possible.
One suggestion was for the audio content to be appended to the existing file, thus growing it in length. A potential issue with this suggestion is the way in which some audio players (e.g. Windows Media Player) seem to cache the length of an audio file the first time it is opened, or added to their internal libraries. This appears to prevent such players from recognising that the length of the audio file has changed, and so will stop playing the track once its original length has elapsed.
Another suggestion was to overwrite a pre-existing file with the new one, though this has many complications. For example, the user may not have intended for this to happen. Even when overwriting is intentional, it can still be problematic. For instance, the existing file might have happened to be opened by another player or utility, and so it would not be possible to overwrite it at that point in time.
A solution to many of these complications is to automatically generate a new file name, based on the original file name. It is also useful to minimise the chance of accidentally generating a file name which might clash with some other existing file name.
The utility now has a new feature which auto generates a new file name if the one specified by the user already exists. For example, if the user specified test7.mp3 as the file name but a file with that name already existed, the utility will now save the new recording to a file called, for example, test7_auto_named_2013_12_29_14_08_36_372.mp3. The auto generated name adds the suffix _auto_named_
followed by the date and time the name was generated. The sequence for the date and time information is: year, month, day, hour, minutes, seconds, and milliseconds. This should help to minimise the chance of unintended file name clashes.
International Languages Support
This request came about after aXu_AP reported a music.mp3 file not found error, which we then tracked down to the fact that the machine the utility was running on used an international language. The language in question was Finnish in this case. I found that the issue was not in the utility itself but rather in mp3_stream.exe which it calls.
I changed the C++ code of mp3_stream.exe and recompiled it, so that it now uses wide characters for all string manipulations. This seemed to improve things, and the name of the line was now being reported as Paavoimakkuus rather than the unprintable characters which were being reported before. However, the issue was still not fixed, because the aa characters in Paavoimakkuus were actually incorrect. The Line name ought to have been Päävoimakkuus. I then found that mp3_stream.exe also hard coded Russian as the encoding language, by calling:
setlocale( LC_ALL, ".866");
I modified mp3_stream.exe again, this time to make it pick up the machine's default language:
setlocale( LC_ALL, "");
This fixed the issue. aXu_AP very kindly tested on the same Finnish machine and sent the screenshots in this section, including this one which shows that the utility now does support international languages.
And Finally...
I hope that you find this utility useful. I have certainly had made a lot of use out of the original mp3_stream.exe application, and thought it would be good to contribute back to CodeProject by posting these usability improvements.
History
- 6th February, 2010
- 18th September, 2011
- Added "Windows 7 Usability Notes" section
- 29th December, 2013
- Added "Feature Requests" section and its "Auto File Names" subsection
- 29th March, 2015
- Updated screenshot, and updated "Windows 7, 8, and 8.1 Usability Notes" section
- 15th November, 2015
- Updated icon, screenshot, and "Windows 7, 8, 8.1, and 10 Usability Notes" section
- Removed solution files compatible with older versions of visual studio (e.g. Record_VS10.sln) now that the latest edition of Visual Studio Community is freely available to all
- Updated code, now targeting .NET 4.6 and C# 6, and making use of async/await, which is discussed in "Recording" section
- 22nd November, 2015
- Changed Save-as behaviour, now that auto file names are generated even if a file already exists
- Made file name persisted to configuration settings storage, again for the same reason
- Fixed duration timer, it is now always in 24-hour format, regardless of machine settings
- Added leading zeros padding to time remaining message in progress bar, for a more consistent look
- Tested on a Windows 10 computer, and updated the article with a screenshot captured on that machine
- 6th March, 2016
- Modified and recompiled mp3_stream.exe to support international languages through wide chars
- 22nd May, 2016
- Added "International Languages Support" section, including screenshots kindly contributed by aXu_AP