Skip to content

Commit

Permalink
feat: add ability to control bitrate, samplerate and channel for audi…
Browse files Browse the repository at this point in the history
…o compression
  • Loading branch information
numandev1 committed Nov 12, 2023
1 parent a01c52c commit 36eec5e
Show file tree
Hide file tree
Showing 16 changed files with 1,210 additions and 125 deletions.
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,17 @@ const result = await Audio.compress(
'file://path_of_file/file_example_MP3_2MG.wav', // recommended wav file but can be use mp3 file
{ quality: 'medium' }
);

// OR

const result = await Audio.compress(
'file://path_of_file/file_example_MP3_2MG.wav', // recommended wav file but can be use mp3 file
{
bitrate: 64000,
samplerate: 44100,
channels: 1,
}
);
```

### Background Upload
Expand Down Expand Up @@ -421,10 +432,20 @@ await clearCache(); // this will clear cache of thumbnails cache directory

### audioCompresssionType

- ###### `quality: qualityType` (default: medium)
- ###### `quality?: qualityType` (default: medium)

we can also control bitrate through quality. qualityType can be `low` | `medium` | `high`

**Note: manual bitrate, samplerate etc will add soon**
- ###### `bitrate?: number` Range [64000-320000]

we can control bitrate of audio through bitrate, it should be in the range of `64000-320000`

- ###### `samplerate?: number` Range [44100 - 192000]

we can control samplerate of audio through samplerate, it should be in the range of `44100 - 192000`

- ###### `channels?: number` Typically 1 or 2
we can control channels of audio through channels, Typically 1 or 2

## Background Upload

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package com.reactnativecompressor.Audio
import android.annotation.SuppressLint
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
import com.naman14.androidlame.LameBuilder
import com.naman14.androidlame.WaveReader
import com.reactnativecompressor.Utils.MediaCache
Expand All @@ -27,21 +28,22 @@ class AudioCompressor {
@JvmStatic
fun CompressAudio(
fileUrl: String,
quality: String,
optionMap: ReadableMap,
context: ReactApplicationContext,
promise: Promise,
) {
var _fileUrl=fileUrl
val realPath = Utils.getRealPath(fileUrl, context)
var _fileUrl=realPath
val filePathWithoutFileUri = realPath!!.replace("file://", "")
try {
val filePath = fileUrl.replace("file://", "")
var wavPath=filePath;
var wavPath=filePathWithoutFileUri;
var isNonWav:Boolean=false
if (fileUrl.endsWith(".mp4", ignoreCase = true))
{
addLog("mp4 file found")
val mp3Path= Utils.generateCacheFilePath("mp3", context)
AudioExtractor().genVideoUsingMuxer(fileUrl, mp3Path, -1, -1, true, false)
_fileUrl="file:/"+mp3Path
_fileUrl=Utils.slashifyFilePath(mp3Path)
wavPath= Utils.generateCacheFilePath("wav", context)
try {
val converter = Converter()
Expand All @@ -58,7 +60,7 @@ class AudioCompressor {
wavPath= Utils.generateCacheFilePath("wav", context)
try {
val converter = Converter()
converter.convert(filePath, wavPath)
converter.convert(filePathWithoutFileUri, wavPath)
} catch (e: JavaLayerException) {
addLog("JavaLayerException error"+e.localizedMessage)
e.printStackTrace();
Expand All @@ -67,7 +69,7 @@ class AudioCompressor {
}


autoCompressHelper(wavPath, quality,context) { mp3Path, finished ->
autoCompressHelper(wavPath,filePathWithoutFileUri, optionMap,context) { mp3Path, finished ->
if (finished) {
val returnableFilePath:String="file://$mp3Path"
addLog("finished: " + returnableFilePath)
Expand All @@ -87,28 +89,23 @@ class AudioCompressor {
}
}

private fun getAudioBitrateByQuality(quality: String): Int {
return when (quality) {
"low" -> 64 // Set your low bitrate value here
"medium" -> 128 // Set your medium bitrate value here
"high" -> 256 // Set your high bitrate value here
else -> 128 // Default to medium bitrate if quality is not recognized
}
}

@SuppressLint("WrongConstant")
private fun autoCompressHelper(
fileUrl: String,
quality: String,
actualFileUrl: String,
optionMap: ReadableMap,
context: ReactApplicationContext,
completeCallback: (String, Boolean) -> Unit
) {

val options = AudioHelper.fromMap(optionMap)
val quality = options.quality

var isCompletedCallbackTriggered:Boolean=false
try {
var mp3Path = Utils.generateCacheFilePath("mp3", context)
val input = File(fileUrl)
val output = File(mp3Path)
val audioBitrate= getAudioBitrateByQuality(quality)

val CHUNK_SIZE = 8192
addLog("Initialising wav reader")
Expand All @@ -122,12 +119,46 @@ class AudioCompressor {
}

addLog("Intitialising encoder")
val androidLame = LameBuilder()
.setInSampleRate(waveReader!!.sampleRate)
.setOutChannels(waveReader!!.channels)
.setOutBitrate(audioBitrate)
.setOutSampleRate(waveReader!!.sampleRate)
.build()


// for bitrate
var audioBitrate:Int
if(options.bitrate != -1)
{
audioBitrate= options.bitrate/1000
}
else
{
audioBitrate=AudioHelper.getDestinationBitrateByQuality(actualFileUrl, quality!!)
Utils.addLog("dest bitrate: $audioBitrate")
}

var androidLame = LameBuilder();
androidLame.setOutBitrate(audioBitrate)

// for channels
var audioChannels:Int
if(options.channels != -1){
audioChannels= options.channels!!
}
else
{
audioChannels=waveReader!!.channels
}
androidLame.setOutChannels(audioChannels)

// for sample rate
androidLame.setInSampleRate(waveReader!!.sampleRate)
var audioSampleRate:Int
if(options.samplerate != -1){
audioSampleRate= options.samplerate!!
}
else
{
audioSampleRate=waveReader!!.sampleRate
}
androidLame.setOutSampleRate(audioSampleRate)
val androidLameBuild=androidLame.build()

try {
outputStream = BufferedOutputStream(FileOutputStream(output), OUTPUT_STREAM_BUFFER)
Expand All @@ -154,7 +185,7 @@ class AudioCompressor {
if (bytesRead > 0) {

var bytesEncoded = 0
bytesEncoded = androidLame.encode(buffer_l, buffer_r, bytesRead, mp3Buf)
bytesEncoded = androidLameBuild.encode(buffer_l, buffer_r, bytesRead, mp3Buf)
addLog("bytes encoded=$bytesEncoded")

if (bytesEncoded > 0) {
Expand All @@ -177,7 +208,7 @@ class AudioCompressor {
if (bytesRead > 0) {
var bytesEncoded = 0

bytesEncoded = androidLame.encode(buffer_l, buffer_l, bytesRead, mp3Buf)
bytesEncoded = androidLameBuild.encode(buffer_l, buffer_l, bytesRead, mp3Buf)
addLog("bytes encoded=$bytesEncoded")

if (bytesEncoded > 0) {
Expand All @@ -202,7 +233,7 @@ class AudioCompressor {
}

addLog("flushing final mp3buffer")
val outputMp3buf = androidLame.flush(mp3Buf)
val outputMp3buf = androidLameBuild.flush(mp3Buf)
addLog("flushed $outputMp3buf bytes")
if (outputMp3buf > 0) {
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
package com.reactnativecompressor.Audio

import android.media.MediaExtractor
import android.media.MediaFormat
import com.facebook.react.bridge.ReadableMap
import com.reactnativecompressor.Utils.Utils
import java.io.File
import java.io.IOException


class AudioHelper {

var quality: String? = "medium"
var bitrate: Int = -1
var samplerate: Int = -1
var channels: Int = -1
var progressDivider: Int? = 0

companion object {
Expand All @@ -15,9 +24,49 @@ class AudioHelper {
val key = iterator.nextKey()
when (key) {
"quality" -> options.quality = map.getString(key)
"bitrate" -> {
val bitrate = map.getInt(key)
options.bitrate = if (bitrate > 320000 || bitrate < 64000) 64000 else bitrate
}
"samplerate" -> options.samplerate = map.getInt(key)
"channels" -> options.channels = map.getInt(key)
}
}
return options
}


fun getAudioBitrate(path: String): Int {
val file = File(path)
val fileSize = file.length() * 8 // size in bits

val mex = MediaExtractor()
try {
mex.setDataSource(path)
} catch (e: IOException) {
e.printStackTrace()
}

val mf = mex.getTrackFormat(0)
val durationUs = mf.getLong(MediaFormat.KEY_DURATION)
val durationSec = durationUs / 1_000_000.0 // convert duration to seconds

return (fileSize / durationSec).toInt()/1000 // bitrate in bits per second
}
fun getDestinationBitrateByQuality(path: String, quality: String): Int {
val originalBitrate = getAudioBitrate(path)
var destinationBitrate = originalBitrate
Utils.addLog("source bitrate: $originalBitrate")

when (quality.toLowerCase()) {
"low" -> destinationBitrate = maxOf(64, (originalBitrate * 0.3).toInt())
"medium" -> destinationBitrate = (originalBitrate * 0.5).toInt()
"high" -> destinationBitrate = minOf(320, (originalBitrate * 0.7).toInt())
else -> Utils.addLog("Invalid quality level. Please enter 'low', 'medium', or 'high'.")
}

return destinationBitrate
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@ package com.reactnativecompressor.Audio
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
import com.reactnativecompressor.Utils.Utils

class AudioMain(private val reactContext: ReactApplicationContext) {
fun compress_audio(
fileUrl: String,
optionMap: ReadableMap,
promise: Promise) {
try {
val options = AudioHelper.fromMap(optionMap)
val quality = options.quality
val realPath = Utils.getRealPath(fileUrl, reactContext)
Utils.addLog(fileUrl + "\n realPath= " + realPath)
AudioCompressor.CompressAudio(realPath!!, quality!!,reactContext,promise)

AudioCompressor.CompressAudio(fileUrl,optionMap,reactContext,promise)
} catch (ex: Exception) {
promise.reject(ex)
}
Expand Down
2 changes: 1 addition & 1 deletion example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,6 @@ SPEC CHECKSUMS:
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Yoga: b76f1acfda8212aa16b7e26bcce3983230c82603

PODFILE CHECKSUM: 6921e9d639a4280c7f39f24a8456ad90b32c9c67
PODFILE CHECKSUM: 686c342866175d212f040c963197ac4ae3a0d39e

COCOAPODS: 1.13.0
7 changes: 6 additions & 1 deletion example/src/Screens/Audio/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ const Index = () => {
setFileName(res[0].name);
setMimeType(res[0].type);
console.log('source file: ', res[0].uri);
Audio.compress(res[0].uri, { quality: 'medium' })
Audio.compress(res[0].uri, {
quality: 'medium',
// bitrate: 64000,
// samplerate: 44100,
// channels: 1,
})
.then(async (outputFilePath: string) => {
console.log(outputFilePath, 'outputFilePath compressed audio');
const detail: any = await getFileInfo(outputFilePath);
Expand Down
49 changes: 49 additions & 0 deletions ios/Audio/AudioHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// AudioHelper.swift
// react-native-compressor
//
// Created by Numan on 12/11/2023.
//

import AVFoundation

class AudioHelper {

static func getAudioBitrate(path: String) -> Int {
let audioURL = URL(fileURLWithPath: path)
let avAsset = AVURLAsset(url: audioURL)
let keys: Set<URLResourceKey> = [.totalFileSizeKey, .fileSizeKey]
let resourceValues = try? audioURL.resourceValues(forKeys: keys)
let fileSize = resourceValues?.fileSize ?? resourceValues?.totalFileSize

// Calculate bitrate in kbps
if let fileSize = fileSize, avAsset.duration.seconds > 0 {
return Int(Double(fileSize) * 8 / avAsset.duration.seconds)
}
return 0
}

static func getDestinationBitrateByQuality(path: String, quality: String) -> Int {
let originalBitrate = getAudioBitrate(path: path)
var destinationBitrate = originalBitrate
print("source bitrate: \(originalBitrate)")

// Calculate the percentage of the original bitrate relative to the range 64000 to 320000
let percentage = Double(originalBitrate - 64000) / Double(320000 - 64000)

switch quality.lowercased() {
case "low":
destinationBitrate = max(64000, Int(Double(originalBitrate) * 0.3))
case "medium":
// Set destination bitrate to 60% of the original bitrate
destinationBitrate = Int(Double(originalBitrate) * 0.5)
case "high":
// Set destination bitrate to 80% of the original bitrate
destinationBitrate = min(320000,Int(Double(originalBitrate) * 0.7))
default:
print("Invalid quality level. Please enter 'low', 'medium', or 'high'.")
}

return destinationBitrate
}
}
Loading

0 comments on commit 36eec5e

Please sign in to comment.