Skip to content

Added sample with static SoX for denoising and normalization #623

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions sox-clean-audio/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log*

# Firebase cache
.firebase/
firebase.json
storage.rules

# Firebase config

# Uncomment this if you'd like others to create their own Firebase project.
# For a team working on the same Firebase project(s), it is recommended to leave
# it commented so all members can deploy to the same project(s) in .firebaserc.
# .firebaserc

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
28 changes: 28 additions & 0 deletions sox-clean-audio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Automatically denoise and normalize audio with SoX
This sample uses a static SoX (Swiss Army knife of sound processing) executable and automatically removes background noise from audio (.WAV format) and then normalizes the audio volume to a given level. The input is supposed to be voice recordings recorded by a smartphone.

## Functions Code

See file [functions/index.js](functions/index.js) for the audio processing code.

The audio processing is performed using a static SoX executable. The audio is first downloaded locally from the Cloud Storage bucket to the `tmp` folder using the [google-cloud](https://github.com/GoogleCloudPlatform/google-cloud-node) SDK.

The dependencies are listed in [functions/package.json](functions/package.json).

## Trigger rules

The function triggers on upload of any file to your Firebase project's default Cloud Storage bucket and perform the process only for .WAV files.

## Deploy and test

To deploy and test the sample:

- Create a Firebase project on the [Firebase Console](https://console.firebase.google.com) and visit the **Storage** tab.
- Get the code, for instance using `git clone https://github.com/firebase/functions-samples`
- Enter the correct directory `cd functions-samples/sox-clean-audio`
- Setup the CLI to use your Firebase project using `firebase use --add` and select your Firebase project
- Deploy your project's code using `firebase deploy`
- Go to the Firebase Console **Storage** tab and upload an audio (with .WAV extension). After a short time a converted audio with the same name but a `d_` prefix for denoized audio files and `n_d_` prefix for denoized and normalized audio files will be created in the same folder (make sure you refresh the UI to see the new file).

## Notes
- The static SoX executable was built on Ubuntu 18.04.03 following [this instruction](https://marcelog.github.io/articles/static_sox_transcoding_lambda_mp3.html).
123 changes: 123 additions & 0 deletions sox-clean-audio/functions/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
{
"parserOptions": {
// Required for certain syntax usages
"ecmaVersion": 2017
},
"plugins": [
"promise"
],
"extends": "eslint:recommended",
"rules": {
// Removed rule "disallow the use of console" from recommended eslint rules
"no-console": "off",

// Removed rule "disallow multiple spaces in regular expressions" from recommended eslint rules
"no-regex-spaces": "off",

// Removed rule "disallow the use of debugger" from recommended eslint rules
"no-debugger": "off",

// Removed rule "disallow unused variables" from recommended eslint rules
"no-unused-vars": "off",

// Removed rule "disallow mixed spaces and tabs for indentation" from recommended eslint rules
"no-mixed-spaces-and-tabs": "off",

// Removed rule "disallow the use of undeclared variables unless mentioned in /*global */ comments" from recommended eslint rules
"no-undef": "off",

// Warn against template literal placeholder syntax in regular strings
"no-template-curly-in-string": 1,

// Warn if return statements do not either always or never specify values
"consistent-return": 1,

// Warn if no return statements in callbacks of array methods
"array-callback-return": 1,

// Require the use of === and !==
"eqeqeq": 2,

// Disallow the use of alert, confirm, and prompt
"no-alert": 2,

// Disallow the use of arguments.caller or arguments.callee
"no-caller": 2,

// Disallow null comparisons without type-checking operators
"no-eq-null": 2,

// Disallow the use of eval()
"no-eval": 2,

// Warn against extending native types
"no-extend-native": 1,

// Warn against unnecessary calls to .bind()
"no-extra-bind": 1,

// Warn against unnecessary labels
"no-extra-label": 1,

// Disallow leading or trailing decimal points in numeric literals
"no-floating-decimal": 2,

// Warn against shorthand type conversions
"no-implicit-coercion": 1,

// Warn against function declarations and expressions inside loop statements
"no-loop-func": 1,

// Disallow new operators with the Function object
"no-new-func": 2,

// Warn against new operators with the String, Number, and Boolean objects
"no-new-wrappers": 1,

// Disallow throwing literals as exceptions
"no-throw-literal": 2,

// Require using Error objects as Promise rejection reasons
"prefer-promise-reject-errors": 2,

// Enforce “for” loop update clause moving the counter in the right direction
"for-direction": 2,

// Enforce return statements in getters
"getter-return": 2,

// Disallow await inside of loops
"no-await-in-loop": 2,

// Disallow comparing against -0
"no-compare-neg-zero": 2,

// Warn against catch clause parameters from shadowing variables in the outer scope
"no-catch-shadow": 1,

// Disallow identifiers from shadowing restricted names
"no-shadow-restricted-names": 2,

// Enforce return statements in callbacks of array methods
"callback-return": 2,

// Require error handling in callbacks
"handle-callback-err": 2,

// Warn against string concatenation with __dirname and __filename
"no-path-concat": 1,

// Prefer using arrow functions for callbacks
"prefer-arrow-callback": 1,

// Return inside each then() to create readable and reusable Promise chains.
// Forces developers to return console logs and http calls in promises.
"promise/always-return": 2,

//Enforces the use of catch() on un-returned promises
"promise/catch-or-return": 2,

// Warn against nested then() or catch() statements
"promise/no-nesting": 1
}
}
1 change: 1 addition & 0 deletions sox-clean-audio/functions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
106 changes: 106 additions & 0 deletions sox-clean-audio/functions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
const functions = require('firebase-functions');
const admin = require("firebase-admin");
const gcs = require('@google-cloud/storage')();
const path = require('path');
const os = require('os');
const fs = require('fs');
const spawn = require('child-process-promise').spawn;

admin.initializeApp(functions.config().firebase);

// Makes a command and return a promise
function promisifyCommand(command) {
return new Promise((resolve, reject) => {
command.on('end', resolve).on('error', reject).run();
});
}

exports.denoiseAudio = functions.storage.object().onFinalize(async (object) => {
const fileBucket = object.bucket;
const filePath = object.name;
const fileName = path.basename(filePath);
const contentType = object.contentType;
const normLevel = "-8";

if (!contentType.startsWith('audio')) {
console.log("This is not an audio")
return null;
}
if (fileName.startsWith('n_') || fileName.startsWith('d_')) {
console.log("This file is already processed");
return null;
}

// Static executable of SoX built on Ubuntu 18.04.3 LTS (GNU/Linux 5.0.0-25-generic x86_64)
const soxPath = [__dirname, 'sox'].join('/');
const bucket = gcs.bucket(fileBucket);
const tempFilePath = path.join(os.tmpdir(), fileName);
const noiseProfPath = path.join(os.tmpdir(), "noise.prof");
const denoisedTempFileName = "d_" + fileName;
const denoisedTempFilePath = path.join(os.tmpdir(), denoisedTempFileName);
const normedTempFileName = "n_" + denoisedTempFileName;
const normedTempFilePath = path.join(os.tmpdir(), normedTempFileName);
const normedStorageFilePath = path.join(path.dirname(filePath), normedTempFileName);

// Download the original audio to your temporary directory
await bucket.file(filePath).download({destination: tempFilePath});
console.log("Audio uploaded locally to ", tempFilePath);

// Compute a noise profile of the original audio
await spawn(soxPath, [
tempFilePath, "-n", "noiseprof", noiseProfPath
], {capture : ['stdout', 'stderr']})
.catch(err => {
console.log(err)
})
console.log("Noise profile created at", noiseProfPath)

// Do denoising in your temporary directory
await spawn(soxPath, [
tempFilePath, denoisedTempFilePath, "noisered", noiseProfPath, "0.1"
], {capture : ['stdout', 'stderr']})
.catch(err => {
console.log(err)
})
console.log("Denoised audio created at", denoisedTempFilePath)

// Do normalization in your temporary directory
await spawn(soxPath, [
`--norm=${normLevel}`, denoisedTempFilePath, normedTempFilePath
], {capture : ['stdout', 'stderr']})
.catch(err => {
console.log(err)
});
console.log('Normalized audio created at', normedTempFilePath);

// Upload the normalized audio to your storage
await bucket.upload(normedTempFilePath, {
destination: normedStorageFilePath,
metadata: {
contentType: "audio/x-wav"
}
});
console.log('Normalized audio uploaded to ', normedStorageFilePath);

// Once the audio has been uploaded delete the local file to free up disk space
fs.unlinkSync(tempFilePath);
fs.unlinkSync(normedTempFilePath);

return console.log('Temporary files removed.', normedTempFilePath)
});
30 changes: 30 additions & 0 deletions sox-clean-audio/functions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "functions",
"description": "Apply audio processing to uploads on Firebase",
"scripts": {
"lint": "eslint .",
"serve": "firebase serve --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "8"
},
"dependencies": {
"@google-cloud/storage": "^0.4.0",
"firebase-admin": "^8.0.0",
"base64url": "^3.0.1",
"child-process-promise": "^2.2.1",
"core-util-is": "^1.0.2",
"firebase-functions": "^3.0.2",
"firebase-tools": "^7.2.4"
},
"devDependencies": {
"eslint": "^5.12.0",
"eslint-plugin-promise": "^4.0.1",
"firebase-functions-test": "^0.1.6"
},
"private": true
}
Binary file added sox-clean-audio/functions/sox
Binary file not shown.