From 9edb1e4322cf1a044bdaaca5d5f3563464d93c5d Mon Sep 17 00:00:00 2001
From: Mikael Finstad <finstaden@gmail.com>
Date: Tue, 8 Dec 2020 13:30:31 +0100
Subject: [PATCH] implement output volume #83

---
 README.md                   |  2 ++
 audio.js                    |  9 +++++----
 cli.js                      |  5 ++++-
 examples/audio-volume.json5 | 11 +++++++++++
 index.js                    |  3 ++-
 5 files changed, 24 insertions(+), 6 deletions(-)
 create mode 100644 examples/audio-volume.json5

diff --git a/README.md b/README.md
index 19207b71..ef30efac 100644
--- a/README.md
+++ b/README.md
@@ -144,6 +144,7 @@ Edit specs are JavaScript / JSON objects describing the whole edit operation wit
   loopAudio: false,
   keepSourceAudio: false,
   clipsAudioVolume: 1,
+  outputVolume: 1,
   audio: [
     {
       path,
@@ -197,6 +198,7 @@ Edit specs are JavaScript / JSON objects describing the whole edit operation wit
 | `loopAudio` | `--loop-audio` | Loop the audio track if it is shorter than video? | `false` | |
 | `keepSourceAudio` | `--keep-source-audio` | Keep source audio from `clips`? | `false` | |
 | `clipsAudioVolume` | | Volume of audio from `clips` relative to `audioTracks`. See [audio tracks](#arbitrary-audio-tracks). | `1` | |
+| `outputVolume` | `--output-volume` | Adjust output [volume](http://ffmpeg.org/ffmpeg-filters.html#volume) (final stage). See [example](https://github.com/mifi/editly/blob/master/examples/audio-volume.json5) | `1` | e.g. `0.5` or `10dB` |
 | `audioNorm.enable` | | Enable audio normalization? See [audio normalization](#audio-normalization). | `false` | |
 | `audioNorm.gaussSize` | | Audio normalization gauss size. See [audio normalization](#audio-normalization). | `5` | |
 | `audioNorm.maxGain` | | Audio normalization max gain. See [audio normalization](#audio-normalization). | `30` | |
diff --git a/audio.js b/audio.js
index 61acff85..87248dc6 100644
--- a/audio.js
+++ b/audio.js
@@ -156,7 +156,7 @@ module.exports = ({ ffmpegPath, ffprobePath, enableFfmpegLog, verbose, tmpDir })
     return outPath;
   }
 
-  async function mixArbitraryAudio({ streams, audioNorm }) {
+  async function mixArbitraryAudio({ streams, audioNorm, outputVolume }) {
     let maxGain = 30;
     let gaussSize = 5;
     if (audioNorm) {
@@ -173,8 +173,9 @@ module.exports = ({ ffmpegPath, ffprobePath, enableFfmpegLog, verbose, tmpDir })
       return `[${i}]atrim=start=${cutFrom || 0}${cutToArg},adelay=delays=${Math.floor((start || 0) * 1000)}:all=1${apadArg}[a${i}]`;
     }).join(';');
 
+    const volumeArg = outputVolume != null ? `,volume=${outputVolume}` : '';
     const audioNormArg = enableAudioNorm ? `,dynaudnorm=g=${gaussSize}:maxgain=${maxGain}` : '';
-    filterComplex += `;${streams.map((s, i) => `[a${i}]`).join('')}amix=inputs=${streams.length}:duration=first:dropout_transition=0:weights=${streams.map((s) => (s.mixVolume != null ? s.mixVolume : 1)).join(' ')}${audioNormArg}`;
+    filterComplex += `;${streams.map((s, i) => `[a${i}]`).join('')}amix=inputs=${streams.length}:duration=first:dropout_transition=0:weights=${streams.map((s) => (s.mixVolume != null ? s.mixVolume : 1)).join(' ')}${audioNormArg}${volumeArg}`;
 
     const mixedAudioPath = join(tmpDir, 'audio-mixed.flac');
 
@@ -198,7 +199,7 @@ module.exports = ({ ffmpegPath, ffprobePath, enableFfmpegLog, verbose, tmpDir })
   }
 
 
-  async function editAudio({ keepSourceAudio, clips, arbitraryAudio, clipsAudioVolume, audioNorm }) {
+  async function editAudio({ keepSourceAudio, clips, arbitraryAudio, clipsAudioVolume, audioNorm, outputVolume }) {
     // We need clips to process audio, because we need to know duration
     if (clips.length === 0) return undefined;
 
@@ -228,7 +229,7 @@ module.exports = ({ ffmpegPath, ffprobePath, enableFfmpegLog, verbose, tmpDir })
 
     if (streams.length < 2) return concatedClipAudioPath;
 
-    const mixedFile = await mixArbitraryAudio({ streams, audioNorm });
+    const mixedFile = await mixArbitraryAudio({ streams, audioNorm, outputVolume });
     return mixedFile;
   }
 
diff --git a/cli.js b/cli.js
index cfe89c10..e526ef37 100644
--- a/cli.js
+++ b/cli.js
@@ -34,6 +34,7 @@ const cli = meow(`
     --audio-file-path  Add an audio track
     --loop-audio  Loop the audio track if it is shorter than video?
     --keep-source-audio  Keep audio from source files
+    --output-volume  Adjust audio output volume
     --allow-remote-requests
 
     --fast, -f  Fast mode (low resolution and FPS, useful for getting a quick preview)
@@ -57,6 +58,7 @@ const cli = meow(`
     height: { type: 'number' },
     fps: { type: 'number' },
     loopAudio: { type: 'boolean' },
+    outputVolume: { type: 'string' },
   },
 });
 
@@ -98,7 +100,7 @@ const cli = meow(`
     params.clips = clips.map((clip) => ({ layers: [clip] }));
   }
 
-  const { verbose, transitionName, transitionDuration, clipDuration, width, height, fps, audioFilePath, fontPath, fast, out: outPath, keepSourceAudio, loopAudio, allowRemoteRequests } = cli.flags;
+  const { verbose, transitionName, transitionDuration, clipDuration, width, height, fps, audioFilePath, fontPath, fast, out: outPath, keepSourceAudio, loopAudio, outputVolume, allowRemoteRequests } = cli.flags;
 
   if (transitionName || transitionDuration != null) {
     params.defaults.transition = {};
@@ -117,6 +119,7 @@ const cli = meow(`
   if (outPath) params.outPath = outPath;
   if (audioFilePath) params.audioFilePath = audioFilePath;
   if (loopAudio) params.loopAudio = loopAudio;
+  if (outputVolume) params.outputVolume = outputVolume;
   if (keepSourceAudio) params.keepSourceAudio = true;
   if (allowRemoteRequests) params.allowRemoteRequests = true;
   if (width) params.width = width;
diff --git a/examples/audio-volume.json5 b/examples/audio-volume.json5
new file mode 100644
index 00000000..032d5267
--- /dev/null
+++ b/examples/audio-volume.json5
@@ -0,0 +1,11 @@
+{
+  outPath: './audio-volume.mp4',
+  width: 200, height: 200,
+  clips: [
+    { duration: 2, layers: [{ type: 'title-background', text: 'Audio output volume' }] },
+  ],
+  audioTracks: [
+    { path: './assets/High [NCS Release] - JPB  (No Copyright Music)-R8ZRCXy5vhA.m4a', cutFrom: 18 },
+  ],
+  outputVolume: '-10dB',
+}
\ No newline at end of file
diff --git a/index.js b/index.js
index 21a78d73..50e417f7 100644
--- a/index.js
+++ b/index.js
@@ -39,6 +39,7 @@ const Editly = async (config = {}) => {
     keepSourceAudio,
     allowRemoteRequests,
     audioNorm,
+    outputVolume,
 
     ffmpegPath = 'ffmpeg',
     ffprobePath = 'ffprobe',
@@ -68,7 +69,7 @@ const Editly = async (config = {}) => {
 
   const { editAudio } = Audio({ ffmpegPath, ffprobePath, enableFfmpegLog, verbose, tmpDir });
 
-  const audioFilePath = !isGif ? await editAudio({ keepSourceAudio, arbitraryAudio, clipsAudioVolume, clips, audioNorm }) : undefined;
+  const audioFilePath = !isGif ? await editAudio({ keepSourceAudio, arbitraryAudio, clipsAudioVolume, clips, audioNorm, outputVolume }) : undefined;
 
   // Try to detect parameters from first video
   let firstVideoWidth;