Skip to content
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

feat: add ability to control bitrate, samplerate and channel for audio compression #237

Merged
merged 1 commit into from
Nov 12, 2023
Merged
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
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,11 +21,16 @@
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);
setCompressedSize(prettyBytes(parseInt(detail.size)));

Check warning on line 33 in example/src/Screens/Audio/index.tsx

View workflow job for this annotation

GitHub Actions / Lint JS (eslint, prettier)

Missing radix parameter.
})
.catch((e) => {
console.log(e, 'error');
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
Loading