diff --git a/README.md b/README.md index 8f6db2a..efa1ccd 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,14 @@ Returns the specified metadata of the provided uri based on the `options` argume > **Note:** The "complicated" typing is to make the resulting promise type-safe and be based off the provided `options`. +### getR128Gain + +```ts +function getR128Gain(uri: string): Promise; +``` + +Attempts to return the track replay gain. + ### saveArtwork ```ts diff --git a/android/src/main/java/com/cyanchill/missingcore/metadataretriever/modules/MetadataRetrieverModule.kt b/android/src/main/java/com/cyanchill/missingcore/metadataretriever/modules/MetadataRetrieverModule.kt index 059fa7a..86c8282 100644 --- a/android/src/main/java/com/cyanchill/missingcore/metadataretriever/modules/MetadataRetrieverModule.kt +++ b/android/src/main/java/com/cyanchill/missingcore/metadataretriever/modules/MetadataRetrieverModule.kt @@ -18,6 +18,7 @@ import com.cyanchill.missingcore.metadataretriever.models.ArtworkOptions import com.cyanchill.missingcore.metadataretriever.models.BridgeReturnables.* import com.cyanchill.missingcore.metadataretriever.utils.MapUtils import com.cyanchill.missingcore.metadataretriever.utils.Normalization +import com.cyanchill.missingcore.metadataretriever.utils.ReplayGainParser import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext @@ -250,7 +251,7 @@ class MetadataRetrieverModule(reactContext: ReactApplicationContext) : metadataEntry.id.uppercase() in ID3v2_LYRIC_TAGS ) { lyricsStr = when (metadataEntry) { - is TextInformationFrame -> metadataEntry.values[0] + is TextInformationFrame -> metadataEntry.values.firstOrNull() is BinaryFrame -> { val byteArr = metadataEntry.data // The 1st byte in the array determines the encoding in ID3. @@ -282,6 +283,8 @@ class MetadataRetrieverModule(reactContext: ReactApplicationContext) : if (isLyricsSync) break } + + if (isLyricsSync) break } if (lyricsStr !== null) lyricsStr = lyricsStr.replace("\u0000", "") @@ -300,6 +303,36 @@ class MetadataRetrieverModule(reactContext: ReactApplicationContext) : } } + override fun getR128Gain(uri: String, promise: Promise) { + try { + val metadataList = getMetadataList(getFormatList(uri)) + var gain: Float? = null + + for (metadata in metadataList) { + val numEntries = metadata.length() + // Manually iterate over metadata entries to find a supported key. + for (i in 0 until numEntries) { + gain = ReplayGainParser(metadata[i]).gain + if (gain != null) break + } + + if (gain != null) break + } + + promise.resolve(gain) + } catch (e: ExecutionException) { + val isWantedException = + e.message?.contains("androidx.media3.datasource.FileDataSource\$FileDataSourceException") + ?: false + when (isWantedException) { + true -> promise.reject("ENOENT", "ENOENT: No such file or directory (${uri})", e) + false -> promise.reject("ERR_REPLAY_GAIN", e.message, e) + } + } catch (e: Exception) { + promise.reject("ERR_REPLAY_GAIN", e.message, e) + } + } + /** Expose to the user the ability to update internal configuration options. */ override fun updateConfigs(options: ReadableMap, promise: Promise) { reader.updateConfigs(Arguments.toBundle(options) as Bundle) diff --git a/android/src/main/java/com/cyanchill/missingcore/metadataretriever/utils/ReplayGainParser.kt b/android/src/main/java/com/cyanchill/missingcore/metadataretriever/utils/ReplayGainParser.kt new file mode 100644 index 0000000..739bea0 --- /dev/null +++ b/android/src/main/java/com/cyanchill/missingcore/metadataretriever/utils/ReplayGainParser.kt @@ -0,0 +1,48 @@ +package com.cyanchill.missingcore.metadataretriever.utils + +import androidx.annotation.OptIn +import androidx.media3.common.Metadata +import androidx.media3.common.util.UnstableApi +import androidx.media3.extractor.metadata.id3.TextInformationFrame +import androidx.media3.extractor.metadata.vorbis.VorbisComment +import androidx.media3.extractor.mp3.Mp3InfoReplayGain + +@OptIn(UnstableApi::class) +class ReplayGainParser(entry: Metadata.Entry) { + var gain: Float? = null + + init { + // Extract track gain based on the class. + gain = when (entry) { + is TextInformationFrame -> handleTextInformationFrame(entry) + is Mp3InfoReplayGain -> handleMp3InfoReplayGain(entry) + is VorbisComment -> handleVorbisComment(entry) + else -> null + } + } + + private fun handleTextInformationFrame(frame: TextInformationFrame): Float? = + when (frame.description?.uppercase()) { + "REPLAYGAIN_TRACK_GAIN" -> frame.values.firstOrNull()?.parseReplayGainAdjustment() + //? R128 gain needs to be divided by `256f`. + "R128_TRACK_GAIN" -> frame.values.firstOrNull()?.parseReplayGainAdjustment()?.div(256f) + else -> null + } + + /** For when we see `ReplayGain Xing/Info`. */ + private fun handleMp3InfoReplayGain(frame: Mp3InfoReplayGain): Float? { + return frame.field1?.gain + } + + private fun handleVorbisComment(comment: VorbisComment): Float? = + when (comment.key.uppercase()) { + "REPLAYGAIN_TRACK_GAIN" -> comment.value.parseReplayGainAdjustment() + //? R128 gain needs to be divided by `256f`. + "R128_TRACK_GAIN" -> comment.value.parseReplayGainAdjustment()?.div(256f) + else -> null + } + + /** Some replay gain tags include "dB" in the string. */ + private fun String.parseReplayGainAdjustment() = + replace(Regex("[^\\d.-]"), "").toFloatOrNull() +} diff --git a/src/NativeMetadataRetriever.ts b/src/NativeMetadataRetriever.ts index f5dfb6d..907ccc5 100644 --- a/src/NativeMetadataRetriever.ts +++ b/src/NativeMetadataRetriever.ts @@ -62,6 +62,8 @@ export interface Spec extends TurboModule { getLyric(uri: string): Promise; + getR128Gain(uri: string): Promise; + updateConfigs(options: ConfigOptions): Promise; /** diff --git a/src/index.ts b/src/index.ts index 39e8d19..bb7f6fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -70,6 +70,13 @@ export async function getLyric(uri: string): Promise { } //#endregion +//#region Replay Gain +/** Returns the replay gain for the track. */ +export async function getR128Gain(uri: string): Promise { + return MetadataRetriever.getR128Gain(uri); +} +//#endregion + //#region Configuration /** Update internal configuration options. */ export async function updateConfigs(options: ConfigOptions): Promise {