Skip to content

Commit 4f07ed8

Browse files
authored
feat: New getR128Gain function to get track replay gain value (#16)
- Validated against `ReplayGain Xing/Info` & `REPLAYGAIN_TRACK_GAIN` in MP3 files. - We also support `R128_TRACK_GAIN`, but have nothing to test against. In addition, both `REPLAYGAIN_TRACK_GAIN` & `R128_TRACK_GAIN` are checked against Vorbis Comments. - In addition, we had some fixes with the lyrics code.
1 parent 41e8395 commit 4f07ed8

5 files changed

Lines changed: 99 additions & 1 deletion

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ Returns the specified metadata of the provided uri based on the `options` argume
101101

102102
> **Note:** The "complicated" typing is to make the resulting promise type-safe and be based off the provided `options`.
103103
104+
### getR128Gain
105+
106+
```ts
107+
function getR128Gain(uri: string): Promise<number | null>;
108+
```
109+
110+
Attempts to return the track replay gain.
111+
104112
### saveArtwork
105113

106114
```ts

android/src/main/java/com/cyanchill/missingcore/metadataretriever/modules/MetadataRetrieverModule.kt

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.cyanchill.missingcore.metadataretriever.models.ArtworkOptions
1818
import com.cyanchill.missingcore.metadataretriever.models.BridgeReturnables.*
1919
import com.cyanchill.missingcore.metadataretriever.utils.MapUtils
2020
import com.cyanchill.missingcore.metadataretriever.utils.Normalization
21+
import com.cyanchill.missingcore.metadataretriever.utils.ReplayGainParser
2122
import com.facebook.react.bridge.Arguments
2223
import com.facebook.react.bridge.Promise
2324
import com.facebook.react.bridge.ReactApplicationContext
@@ -250,7 +251,7 @@ class MetadataRetrieverModule(reactContext: ReactApplicationContext) :
250251
metadataEntry.id.uppercase() in ID3v2_LYRIC_TAGS
251252
) {
252253
lyricsStr = when (metadataEntry) {
253-
is TextInformationFrame -> metadataEntry.values[0]
254+
is TextInformationFrame -> metadataEntry.values.firstOrNull()
254255
is BinaryFrame -> {
255256
val byteArr = metadataEntry.data
256257
// The 1st byte in the array determines the encoding in ID3.
@@ -282,6 +283,8 @@ class MetadataRetrieverModule(reactContext: ReactApplicationContext) :
282283

283284
if (isLyricsSync) break
284285
}
286+
287+
if (isLyricsSync) break
285288
}
286289

287290
if (lyricsStr !== null) lyricsStr = lyricsStr.replace("\u0000", "")
@@ -300,6 +303,36 @@ class MetadataRetrieverModule(reactContext: ReactApplicationContext) :
300303
}
301304
}
302305

306+
override fun getR128Gain(uri: String, promise: Promise) {
307+
try {
308+
val metadataList = getMetadataList(getFormatList(uri))
309+
var gain: Float? = null
310+
311+
for (metadata in metadataList) {
312+
val numEntries = metadata.length()
313+
// Manually iterate over metadata entries to find a supported key.
314+
for (i in 0 until numEntries) {
315+
gain = ReplayGainParser(metadata[i]).gain
316+
if (gain != null) break
317+
}
318+
319+
if (gain != null) break
320+
}
321+
322+
promise.resolve(gain)
323+
} catch (e: ExecutionException) {
324+
val isWantedException =
325+
e.message?.contains("androidx.media3.datasource.FileDataSource\$FileDataSourceException")
326+
?: false
327+
when (isWantedException) {
328+
true -> promise.reject("ENOENT", "ENOENT: No such file or directory (${uri})", e)
329+
false -> promise.reject("ERR_REPLAY_GAIN", e.message, e)
330+
}
331+
} catch (e: Exception) {
332+
promise.reject("ERR_REPLAY_GAIN", e.message, e)
333+
}
334+
}
335+
303336
/** Expose to the user the ability to update internal configuration options. */
304337
override fun updateConfigs(options: ReadableMap, promise: Promise) {
305338
reader.updateConfigs(Arguments.toBundle(options) as Bundle)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.cyanchill.missingcore.metadataretriever.utils
2+
3+
import androidx.annotation.OptIn
4+
import androidx.media3.common.Metadata
5+
import androidx.media3.common.util.UnstableApi
6+
import androidx.media3.extractor.metadata.id3.TextInformationFrame
7+
import androidx.media3.extractor.metadata.vorbis.VorbisComment
8+
import androidx.media3.extractor.mp3.Mp3InfoReplayGain
9+
10+
@OptIn(UnstableApi::class)
11+
class ReplayGainParser(entry: Metadata.Entry) {
12+
var gain: Float? = null
13+
14+
init {
15+
// Extract track gain based on the class.
16+
gain = when (entry) {
17+
is TextInformationFrame -> handleTextInformationFrame(entry)
18+
is Mp3InfoReplayGain -> handleMp3InfoReplayGain(entry)
19+
is VorbisComment -> handleVorbisComment(entry)
20+
else -> null
21+
}
22+
}
23+
24+
private fun handleTextInformationFrame(frame: TextInformationFrame): Float? =
25+
when (frame.description?.uppercase()) {
26+
"REPLAYGAIN_TRACK_GAIN" -> frame.values.firstOrNull()?.parseReplayGainAdjustment()
27+
//? R128 gain needs to be divided by `256f`.
28+
"R128_TRACK_GAIN" -> frame.values.firstOrNull()?.parseReplayGainAdjustment()?.div(256f)
29+
else -> null
30+
}
31+
32+
/** For when we see `ReplayGain Xing/Info`. */
33+
private fun handleMp3InfoReplayGain(frame: Mp3InfoReplayGain): Float? {
34+
return frame.field1?.gain
35+
}
36+
37+
private fun handleVorbisComment(comment: VorbisComment): Float? =
38+
when (comment.key.uppercase()) {
39+
"REPLAYGAIN_TRACK_GAIN" -> comment.value.parseReplayGainAdjustment()
40+
//? R128 gain needs to be divided by `256f`.
41+
"R128_TRACK_GAIN" -> comment.value.parseReplayGainAdjustment()?.div(256f)
42+
else -> null
43+
}
44+
45+
/** Some replay gain tags include "dB" in the string. */
46+
private fun String.parseReplayGainAdjustment() =
47+
replace(Regex("[^\\d.-]"), "").toFloatOrNull()
48+
}

src/NativeMetadataRetriever.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ export interface Spec extends TurboModule {
6262

6363
getLyric(uri: string): Promise<string | null>;
6464

65+
getR128Gain(uri: string): Promise<number | null>;
66+
6567
updateConfigs(options: ConfigOptions): Promise<void>;
6668

6769
/**

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ export async function getLyric(uri: string): Promise<string | null> {
7070
}
7171
//#endregion
7272

73+
//#region Replay Gain
74+
/** Returns the replay gain for the track. */
75+
export async function getR128Gain(uri: string): Promise<number | null> {
76+
return MetadataRetriever.getR128Gain(uri);
77+
}
78+
//#endregion
79+
7380
//#region Configuration
7481
/** Update internal configuration options. */
7582
export async function updateConfigs(options: ConfigOptions): Promise<void> {

0 commit comments

Comments
 (0)