C# FFmpeg Opus Audio Conversion Utility
The snippet can be accessed without any authentication.
Authored by
Mohammad Mahdi Mohammadi
A C# utility class for high-quality audio conversion to Opus format using FFmpeg. This implementation provides multiple conversion methods with proper async/await patterns, error handling, and resource management. All vibe coded.
FfmpegOpusConverter.cs 18.64 KiB
using System.Diagnostics;
using System.IO;
namespace vcecht_exp_helper;
public static class FfmpegOpusConverter
{
/// <summary>
/// Asynchronously converts a WAV (or any PCM) stream into an Opus stream.
/// </summary>
/// <param name="inputStream">
/// Any Stream (e.g. FileStream, MemoryStream, NetworkStream) containing raw audio
/// FFmpeg can decode (here assumed WAV/PCM).
/// </param>
/// <param name="cancellationToken">
/// CancellationToken to abort the operation mid‐stream.
/// </param>
/// <returns>
/// A MemoryStream containing the Opus output. The returned stream’s Position is set to 0.
/// </returns>
public static async Task<Stream> ConvertWavToOpusAsync(
Stream inputStream,
CancellationToken cancellationToken = default)
{
if (inputStream == null) throw new ArgumentNullException(nameof(inputStream));
// Build ffmpeg arguments (quiet, read stdin, write stdout in Opus)
string ffmpegArgs = string.Join(" ", new[]
{
"-hide_banner",
"-loglevel", "error",
"-nostdin",
"-i", "pipe:0",
"-c:a", "libopus",
"-b:a", "16k",
"-compression_level", "10",
"-vbr", "on",
"-application", "voip",
"-frame_size", "20",
"-deadline", "realtime",
"-ar", "24000",
"-f", "opus",
"-fflags"," +genpts",
"pipe:1"
});
var psi = new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = ffmpegArgs,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
var ffmpeg = new Process { StartInfo = psi, EnableRaisingEvents = true };
try
{
if (!ffmpeg.Start())
throw new InvalidOperationException("Failed to start FFmpeg process.");
//
// 1) Write inputStream → ffmpeg.StandardInput.BaseStream
//
Task writeStdin = Task.Run(async () =>
{
try
{
Stream stdinPipe = ffmpeg.StandardInput.BaseStream;
await inputStream.CopyToAsync(stdinPipe, bufferSize: 81920, cancellationToken).ConfigureAwait(false);
// Signal EOF to ffmpeg
ffmpeg.StandardInput.Close();
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
// If writing fails, kill the process so it doesn’t hang
try { ffmpeg.Kill(); } catch { /* ignore */ }
throw;
}
}, cancellationToken);
//
// 2) Read ffmpeg.StandardError (to avoid deadlocks and capture any error text)
//
Task<string> readStderr = Task.Run(async () =>
{
using (var sr = ffmpeg.StandardError)
{
return await sr.ReadToEndAsync().ConfigureAwait(false);
}
}, cancellationToken);
//
// 3) Read ffmpeg.StandardOutput → MemoryStream
//
var outputMem = new MemoryStream();
Task readStdout = Task.Run(async () =>
{
try
{
Stream stdoutPipe = ffmpeg.StandardOutput.BaseStream;
await stdoutPipe.CopyToAsync(outputMem, bufferSize: 81920, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
try { ffmpeg.Kill(); } catch { /* ignore */ }
throw;
}
}, cancellationToken);
// Wait for both copying tasks to finish
await Task.WhenAll(writeStdin, readStdout).ConfigureAwait(false);
// Now wait for ffmpeg to exit
await Task.Run(() => ffmpeg.WaitForExit(), cancellationToken).ConfigureAwait(false);
if (ffmpeg.ExitCode != 0)
{
string stderrText = await readStderr.ConfigureAwait(false);
throw new InvalidOperationException(
$"FFmpeg exited with code {ffmpeg.ExitCode}. Details:\n{stderrText}");
}
// Rewind and return
outputMem.Position = 0;
return outputMem;
}
finally
{
ffmpeg.Dispose();
}
}
/// <summary>
/// Asynchronously converts a WAV (or any PCM) stream into an Opus file.
/// </summary>
/// <param name="inputStream">
/// Any Stream (e.g. FileStream, MemoryStream, NetworkStream) containing raw audio
/// FFmpeg can decode (here assumed WAV/PCM).
/// </param>
/// <param name="outputFileName">
/// The name of the output file (without path).
/// </param>
/// <param name="cancellationToken">
/// CancellationToken to abort the operation mid‐stream.
/// </param>
/// <returns>
/// The absolute path to the generated Opus file.
/// </returns>
public static async Task<string> ConvertWavToOpusFileAsync(
Stream inputStream,
string outputFileName,
CancellationToken cancellationToken = default)
{
if (inputStream == null) throw new ArgumentNullException(nameof(inputStream));
if (string.IsNullOrWhiteSpace(outputFileName))
throw new ArgumentException("Output file name cannot be empty", nameof(outputFileName));
// Ensure the output directory exists
string outputDir = "/tmp/vcecht";
Directory.CreateDirectory(outputDir);
string outputPath = Path.Combine(outputDir, outputFileName);
// Build ffmpeg arguments (quiet, read stdin, write to file in Opus)
string ffmpegArgs = string.Join(" ", new[]
{
"-hide_banner",
"-loglevel", "error",
"-nostdin",
"-i", "pipe:0",
"-c:a", "libopus",
"-b:a", "16k",
"-compression_level", "10",
"-vbr", "on",
"-application", "voip",
"-frame_duration", "20",
"-movflags", "+faststart",
"-ar", "24000",
"-f", "Opus",
$"\"{outputPath}\"" // Output to file instead of stdout
});
var psi = new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = ffmpegArgs,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
var ffmpeg = new Process { StartInfo = psi, EnableRaisingEvents = true };
try
{
if (!ffmpeg.Start())
throw new InvalidOperationException("Failed to start FFmpeg process.");
// Write inputStream → ffmpeg.StandardInput.BaseStream
await Task.Run(async () =>
{
try
{
Stream stdinPipe = ffmpeg.StandardInput.BaseStream;
await inputStream.CopyToAsync(stdinPipe, bufferSize: 81920, cancellationToken).ConfigureAwait(false);
// Signal EOF to ffmpeg
ffmpeg.StandardInput.Close();
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
// If writing fails, kill the process so it doesn't hang
try { ffmpeg.Kill(); } catch { /* ignore */ }
throw;
}
}, cancellationToken);
// Read stderr to capture any error text
string stderrText = await Task.Run(async () =>
{
using (var sr = ffmpeg.StandardError)
{
return await sr.ReadToEndAsync().ConfigureAwait(false);
}
}, cancellationToken);
// Wait for ffmpeg to finish
await Task.Run(() => ffmpeg.WaitForExit(), cancellationToken).ConfigureAwait(false);
if (ffmpeg.ExitCode != 0)
{
throw new InvalidOperationException(
$"FFmpeg exited with code {ffmpeg.ExitCode}. Details:\n{stderrText}");
}
// Verify the file was created
if (!File.Exists(outputPath))
{
throw new InvalidOperationException("FFmpeg did not create the expected output file.");
}
return outputPath;
}
finally
{
ffmpeg.Dispose();
}
}
/// <summary>
/// Reads a file from the specified path and returns it as a memory stream.
/// </summary>
/// <param name="filePath">The path to the file to read.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A MemoryStream containing the file contents.</returns>
/// <exception cref="ArgumentNullException">Thrown when filePath is null or empty.</exception>
/// <exception cref="FileNotFoundException">Thrown when the specified file does not exist.</exception>
public static async Task<MemoryStream> ReadFileToStreamAsync(string filePath, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentNullException(nameof(filePath));
if (!File.Exists(filePath))
throw new FileNotFoundException($"The file '{filePath}' was not found.", filePath);
var memoryStream = new MemoryStream();
try
{
using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 81920, FileOptions.Asynchronous | FileOptions.SequentialScan))
{
await fileStream.CopyToAsync(memoryStream, 81920, cancellationToken).ConfigureAwait(false);
}
memoryStream.Position = 0; // Reset position to the beginning of the stream
return memoryStream;
}
catch
{
await memoryStream.DisposeAsync();
throw;
}
}
/// <summary>
/// Writes the contents of a stream to the specified file path.
/// </summary>
/// <param name="stream">The stream to write to the file.</param>
/// <param name="filePath">The path where the file will be written.</param>
/// <param name="overwrite">Whether to overwrite the file if it already exists. Default is true.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
/// <exception cref="ArgumentNullException">Thrown when stream or filePath is null or empty.</exception>
/// <exception cref="IOException">Thrown when the file already exists and overwrite is false.</exception>
public static async Task WriteStreamToFileAsync(Stream stream, string filePath, bool overwrite = true, CancellationToken cancellationToken = default)
{
if (stream == null)
throw new ArgumentNullException(nameof(stream));
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentNullException(nameof(filePath));
// Ensure the directory exists
var directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
if (File.Exists(filePath))
{
if (!overwrite)
throw new IOException($"The file '{filePath}' already exists and overwrite is set to false.");
File.Delete(filePath);
}
using (var fileStream = new FileStream(
filePath,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None,
81920,
FileOptions.Asynchronous | FileOptions.SequentialScan))
{
await stream.CopyToAsync(fileStream, 81920, cancellationToken).ConfigureAwait(false);
await fileStream.FlushAsync(cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Converts an audio file to Opus format by reading and writing directly from/to files.
/// </summary>
/// <param name="inputFilePath">Path to the input audio file.</param>
/// <param name="outputFilePath">Path where the output Opus file will be saved.</param>
/// <param name="cancellationToken">CancellationToken to abort the operation.</param>
/// <returns>The path to the converted file (same as outputFilePath).</returns>
public static async Task<string> ConvertFileToOpusFileAsync(
string inputFilePath,
string outputFilePath,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(inputFilePath))
throw new ArgumentNullException(nameof(inputFilePath));
if (string.IsNullOrWhiteSpace(outputFilePath))
throw new ArgumentNullException(nameof(outputFilePath));
if (!File.Exists(inputFilePath))
throw new FileNotFoundException($"Input file not found: {inputFilePath}", inputFilePath);
// Ensure the output directory exists
string outputDir = Path.GetDirectoryName(outputFilePath);
if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir))
{
Directory.CreateDirectory(outputDir);
}
// Build ffmpeg arguments
string ffmpegArgs = string.Join(" ", new[]
{
"-hide_banner",
"-loglevel", "error",
"-i", $"\"{inputFilePath}\"",
"-c:a", "libopus",
"-b:a", "16k",
"-compression_level", "10",
"-vbr", "on",
"-application", "voip",
"-frame_size", "20",
"-deadline", "realtime",
"-ar", "24000",
"-f", "opus",
"-fflags"," +genpts",
$"\"{outputFilePath}\""
});
var psi = new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = ffmpegArgs,
UseShellExecute = false,
RedirectStandardError = true,
CreateNoWindow = true
};
using var ffmpeg = new Process { StartInfo = psi, EnableRaisingEvents = true };
if (!ffmpeg.Start())
throw new InvalidOperationException("Failed to start FFmpeg process.");
// Read stderr to capture any error text
string stderrText = await Task.Run(async () =>
{
using var sr = ffmpeg.StandardError;
return await sr.ReadToEndAsync().ConfigureAwait(false);
}, cancellationToken);
// Wait for ffmpeg to finish
await Task.Run(() => ffmpeg.WaitForExit(), cancellationToken).ConfigureAwait(false);
if (ffmpeg.ExitCode != 0)
{
throw new InvalidOperationException(
$"FFmpeg exited with code {ffmpeg.ExitCode}. Details:\n{stderrText}");
}
if (!File.Exists(outputFilePath))
{
throw new InvalidOperationException("FFmpeg did not create the expected output file.");
}
return outputFilePath;
}
/// <summary>
/// Converts an audio file to Opus format and returns it as a stream.
/// </summary>
/// <param name="inputFilePath">Path to the input audio file.</param>
/// <param name="cancellationToken">CancellationToken to abort the operation.</param>
/// <returns>A MemoryStream containing the converted audio in Opus format.</returns>
public static async Task<Stream> ConvertFileToOpusStreamAsync(
string inputFilePath,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(inputFilePath))
throw new ArgumentNullException(nameof(inputFilePath));
if (!File.Exists(inputFilePath))
throw new FileNotFoundException($"Input file not found: {inputFilePath}", inputFilePath);
// Build ffmpeg arguments
string ffmpegArgs = string.Join(" ", new[]
{
"-hide_banner",
"-loglevel", "error",
"-i", $"\"{inputFilePath}\"",
"-c:a", "libopus",
"-b:a", "16k",
"-compression_level", "10",
"-vbr", "on",
"-application", "voip",
"-frame_size", "20",
"-deadline", "realtime",
"-ar", "24000",
"-f", "opus",
"-fflags"," +genpts",
"pipe:1"
});
var psi = new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = ffmpegArgs,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
var outputMem = new MemoryStream();
var ffmpeg = new Process { StartInfo = psi, EnableRaisingEvents = true };
try
{
if (!ffmpeg.Start())
throw new InvalidOperationException("Failed to start FFmpeg process.");
// Read stdout into memory stream
await Task.Run(async () =>
{
try
{
await ffmpeg.StandardOutput.BaseStream.CopyToAsync(outputMem, 81920, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
try { ffmpeg.Kill(); } catch { /* ignore */ }
throw;
}
}, cancellationToken);
// Read stderr to capture any error text
string stderrText = await Task.Run(async () =>
{
using var sr = ffmpeg.StandardError;
return await sr.ReadToEndAsync().ConfigureAwait(false);
}, cancellationToken);
// Wait for ffmpeg to finish
await Task.Run(() => ffmpeg.WaitForExit(), cancellationToken).ConfigureAwait(false);
if (ffmpeg.ExitCode != 0)
{
throw new InvalidOperationException(
$"FFmpeg exited with code {ffmpeg.ExitCode}. Details:\n{stderrText}");
}
outputMem.Position = 0;
return outputMem;
}
catch
{
await outputMem.DisposeAsync();
throw;
}
finally
{
ffmpeg.Dispose();
}
}
}
Please register or sign in to comment