Skip to content
Snippets Groups Projects

C# FFmpeg Opus Audio Conversion Utility

  • Clone with SSH
  • Clone with HTTPS
  • Embed
  • Share
    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();
            }
        }
    }
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Please register or to comment