C# ffmpeg loudnorm 2 pass script

This is a C# script to normalize audio volume with two pass. The script first runs the ffmpeg to parse measured_I, measured_TP, measured_LRA and measured_thresh. Then it uses these values to run a second pass and produce a normalized output wave file. Everything happens automated!
(Rev. 19-Mar-2024)

Categories | About |     |  

Parveen,

Pre-requisites

  1. Only Windows 7 and later. Prefer 64 bit.
  2. Visual Studio with C# - any DOTNET Framework.
  3. Get ffmpeg from here - https://ffmpeg.zeranoe.com/builds/
  4. You should be well versed with writing and tweaking code.
  5. Feel free to optimize the code!

Compilation Notes

  1. PREPEND_AUDIO_FILTER can be String.Empty
  2. IN_WAV is the name of the input wav file.
  3. OUT_IS_MONO can be false for stereo output.
  4. DUALMONO - read the documentation on ffmpeg pages or read this https://developers.google.com/assistant/tools/audio-loudness
  5. Create a new C# console application and paste the below code and compile.
  6. The compiled exe and ffmpeg.exe and IN_WAV should be in the same folder.

The Script

This is ready to copy and paste:

using System;

using System.Collections.Generic;

using System.Diagnostics;

using System.IO;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

namespace FFMPEGProducer
{

  class Program
  {

    private const String DESIRED_I = "-16";

    // desired TP 
    private const String DESIRED_TP = "-1.5";

    private const String DESIRED_LRA = "11";

    // notice the comma at the end 
    private const String PREPEND_AUDIO_FILTER = "treble=gain=3,bass=gain=-3,";

    // input wave file 
    private const String IN_WAV = "in.wav";

    // name of the normalized output file 
    private const String OUT_NORMALIZED_FILE = "out.wav";

    // on one channel is output for speech or lecture 
    private const bool OUT_IS_MONO = true;

    // mono/stereo correction 
    // read documentation if you need it 
    private const bool DUALMONO = false;

    static void Main(string[] args)
    {

      Program p = new Program();

      // delete the output file if it exists 
      File.Delete(OUT_NORMALIZED_FILE);

      // first pass 
      String measured_I = String.Empty, measured_TP = String.Empty, measured_LRA = String.Empty, measured_thresh = String.Empty, offset = String.Empty;

      using (Process process = new Process())

      {

        process.StartInfo.FileName = "ffmpeg.exe";

        process.StartInfo.Arguments = String.Format("-i \"{0}\" -af \"{1}loudnorm=I={2}{5}:TP={3}:LRA={4}:print_format=summary\" {6}  -f null -", IN_WAV, PREPEND_AUDIO_FILTER, DESIRED_I, DESIRED_TP, DESIRED_LRA, (DUALMONO ? ":dual_mono=true" : String.Empty), (OUT_IS_MONO ? " -ac 1 " : String.Empty));

        process.StartInfo.UseShellExecute = false;

        process.StartInfo.RedirectStandardOutput = true;

        process.StartInfo.RedirectStandardError = true;

        process.StartInfo.CreateNoWindow = true;

        process.ErrorDataReceived += (s, e) =>
        {

          String strLine = e.Data;

          if (!String.IsNullOrEmpty(strLine))
          {

            Console.WriteLine(strLine);

            if (strLine.StartsWith("Input Integrated:"))
            {

              measured_I = p.ExtractNumberFromFFMPEGLine(strLine);

            }

            else if (strLine.StartsWith("Input True"))
            {

              measured_TP = p.ExtractNumberFromFFMPEGLine(strLine);

            }

            else if (strLine.StartsWith("Input LRA:"))
            {

              measured_LRA = p.ExtractNumberFromFFMPEGLine(strLine);

            }

            else if (strLine.StartsWith("Input Threshold:"))
            {

              measured_thresh = p.ExtractNumberFromFFMPEGLine(strLine);

            }

            else if (strLine.StartsWith("Target"))
            {

              offset = p.ExtractNumberFromFFMPEGLine(strLine);

            }

          }

        };

        process.Start();

        process.BeginOutputReadLine();

        process.BeginErrorReadLine();

        process.WaitForExit();

      }

      if (!String.IsNullOrEmpty(p.strError))
      {

        Console.WriteLine("Fatal Error: " + p.strError);

        goto lblExit;

      }

      // second pass 
      String cmd = String.Format("-i \"{0}\" -af \"{1}loudnorm=I={2}{5}:TP={3}:LRA={4}:measured_I={7}:measured_TP={8}:measured_LRA={9}:measured_thresh={10}:offset={11}:linear=true:print_format=summary\" {6}  -y {12}", IN_WAV, PREPEND_AUDIO_FILTER, DESIRED_I, DESIRED_TP, DESIRED_LRA, (DUALMONO ? ":dual_mono=true" : String.Empty), (OUT_IS_MONO ? " -ac 1 " : String.Empty), measured_I, measured_TP, measured_LRA, measured_thresh, offset, OUT_NORMALIZED_FILE);

      Console.WriteLine(cmd);

      p.ExecuteCommand(cmd);

    lblExit:
      Console.WriteLine("Press enter to exit . . . ");

      Console.ReadLine();

    }

    String strError;

    void ExecuteCommand(string command)
    {

      using (Process process = new Process())

      {

        process.StartInfo.FileName = "ffmpeg.exe";

        process.StartInfo.Arguments = command;

        process.StartInfo.UseShellExecute = false;

        process.StartInfo.RedirectStandardOutput = true;

        process.StartInfo.RedirectStandardError = true;

        process.StartInfo.CreateNoWindow = true;

        process.OutputDataReceived += (s, e) =>
        {

          String strLine = e.Data;

          if (!String.IsNullOrEmpty(strLine))
          {

            Console.WriteLine(strLine);

          }

        };

        process.ErrorDataReceived += (s, e) =>
        {

          String strLine = e.Data;

          if (!String.IsNullOrEmpty(strLine))
          {

            Console.WriteLine(strLine);

          }

        };

        process.Start();

        process.BeginOutputReadLine();

        process.BeginErrorReadLine();

        process.WaitForExit();

      }

    }

    String ExtractNumberFromFFMPEGLine (String strLine)
    {

      StringBuilder sbNumber = new StringBuilder();

      foreach (Char k in strLine)
      {

        if (char.IsDigit(k) || ('.' == k) || ('-' == k))
        {

          sbNumber.Append(k);

        }

      }

      String ret = sbNumber.ToString();

      try
      {

        float.Parse(ret);

      }

      catch
      {

        if (String.IsNullOrEmpty(strError))
        {

          strError = "Failed parse " + strLine;

        }

      }

      return ret;

    }

  }

}


This Blog Post/Article "C# ffmpeg loudnorm 2 pass script" by Parveen is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.