react-native-compressor 1.19.2 → 1.19.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <div align="center">
2
2
  <img height="150" src="/media/logo.png" />
3
3
  </div>
4
-
4
+
5
5
  <br/>
6
6
 
7
7
  <div align="center">
@@ -14,6 +14,12 @@
14
14
 
15
15
  </div>
16
16
 
17
+ > ⚠️ **Legacy Notice**
18
+ >
19
+ > This repository is now in **maintenance mode** for the v1 series. Version **1.19.3** will be the last release of v1. Development has moved to **[v2](https://github.com/numandev1/react-native-compressor)** which uses **Nitro Modules** for improved performance and maintainability.
20
+ >
21
+ > New features, bug fixes and enhancements will land in v2 only.
22
+
17
23
  **REACT-NATIVE-COMPRESSOR** is a react-native package, which helps us to Compress `Image`, `Video`, and `Audio` before uploading, same like **Whatsapp** without knowing the compression `algorithm`
18
24
 
19
25
  <div align="center">
@@ -119,7 +119,6 @@ dependencies {
119
119
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
120
120
  implementation 'org.mp4parser:isoparser:1.9.56'
121
121
  implementation 'com.github.kaushik-naik:TAndroidLame:277c2ab4b0'
122
- implementation 'javazoom:jlayer:1.0.1'
123
122
  }
124
123
 
125
124
  if (isNewArchitectureEnabled()) {
@@ -10,8 +10,6 @@ import com.naman14.androidlame.WaveReader
10
10
  import com.reactnativecompressor.Utils.MediaCache
11
11
  import com.reactnativecompressor.Utils.Utils
12
12
  import com.reactnativecompressor.Utils.Utils.addLog
13
- import javazoom.jl.converter.Converter
14
- import javazoom.jl.decoder.JavaLayerException
15
13
  import java.io.BufferedOutputStream
16
14
  import java.io.File
17
15
  import java.io.FileNotFoundException
@@ -20,11 +18,12 @@ import java.io.IOException
20
18
 
21
19
  class AudioCompressor {
22
20
  companion object {
23
- val TAG="AudioMain"
21
+ val TAG = "AudioMain"
24
22
  private const val OUTPUT_STREAM_BUFFER = 8192
25
23
 
26
24
  var outputStream: BufferedOutputStream? = null
27
25
  var waveReader: WaveReader? = null
26
+
28
27
  @JvmStatic
29
28
  fun CompressAudio(
30
29
  fileUrl: String,
@@ -33,59 +32,76 @@ class AudioCompressor {
33
32
  promise: Promise,
34
33
  ) {
35
34
  val realPath = Utils.getRealPath(fileUrl, context)
36
- var _fileUrl=realPath
35
+ var _fileUrl = realPath
37
36
  val filePathWithoutFileUri = realPath!!.replace("file://", "")
37
+
38
+ // These are declared outside the try block so they can be cleaned up
39
+ // in the catch handler. They must be nullable because the transcoding
40
+ // branch may not execute.
41
+ var wavPath: String? = null
42
+ var isNonWav: Boolean = false
43
+
38
44
  try {
39
- var wavPath=filePathWithoutFileUri;
40
- var isNonWav:Boolean=false
41
- if (fileUrl.endsWith(".mp4", ignoreCase = true))
42
- {
43
- addLog("mp4 file found")
44
- val mp3Path= Utils.generateCacheFilePath("mp3", context)
45
- AudioExtractor().genVideoUsingMuxer(fileUrl, mp3Path, -1, -1, true, false)
46
- _fileUrl=Utils.slashifyFilePath(mp3Path)
47
- wavPath= Utils.generateCacheFilePath("wav", context)
45
+ wavPath = filePathWithoutFileUri
46
+ // LAME's WaveReader requires PCM 16-bit LE WAV. Anything else
47
+ // (m4a/AAC, mp3, ogg, flac, video audio tracks, ...) must be
48
+ // decoded to WAV first via the platform MediaCodec pipeline —
49
+ // jlayer's MP3-only Converter silently produced empty WAVs for
50
+ // every non-MP3 input, which LAME then encoded as a few seconds
51
+ // of silence (see #410 thread, m4a/AAC support).
52
+ //
53
+ // Check the resolved local path, not the original fileUrl: a
54
+ // content:// or remote URL won't carry the file extension, so
55
+ // checking fileUrl would mis-classify WAV inputs as non-WAV and
56
+ // route them through an unnecessary (and potentially failing)
57
+ // MediaCodec decode pass.
58
+ if (!filePathWithoutFileUri.endsWith(".wav", ignoreCase = true)) {
59
+ addLog("non wav file found, transcoding to PCM WAV")
60
+ wavPath = Utils.generateCacheFilePath("wav", context)
48
61
  try {
49
- val converter = Converter()
50
- converter.convert(mp3Path, wavPath)
51
- } catch (e: JavaLayerException) {
52
- addLog("JavaLayerException error"+e.localizedMessage)
53
- e.printStackTrace();
62
+ AudioTranscoder.decodeToWav(filePathWithoutFileUri, wavPath)
63
+ } catch (e: IOException) {
64
+ addLog("AudioTranscoder failed: " + e.localizedMessage)
65
+ // Surface the failure rather than silently returning a
66
+ // degenerate MP3 of silence.
67
+ File(wavPath).delete()
68
+ promise.reject("audio_transcode_failed", e.localizedMessage ?: "Audio decode failed", e)
69
+ return
54
70
  }
55
- isNonWav=true
56
- }
57
- else if (!fileUrl.endsWith(".wav", ignoreCase = true))
58
- {
59
- addLog("non wav file found")
60
- wavPath= Utils.generateCacheFilePath("wav", context)
61
- try {
62
- val converter = Converter()
63
- converter.convert(filePathWithoutFileUri, wavPath)
64
- } catch (e: JavaLayerException) {
65
- addLog("JavaLayerException error"+e.localizedMessage)
66
- e.printStackTrace();
67
- }
68
- isNonWav=true
71
+ isNonWav = true
69
72
  }
70
73
 
71
-
72
- autoCompressHelper(wavPath,filePathWithoutFileUri, optionMap,context) { mp3Path, finished ->
74
+ autoCompressHelper(wavPath, filePathWithoutFileUri, optionMap, context) { mp3Path, finished ->
73
75
  if (finished) {
74
- val returnableFilePath:String="file://$mp3Path"
76
+ val returnableFilePath: String = "file://$mp3Path"
75
77
  addLog("finished: " + returnableFilePath)
76
78
  MediaCache.removeCompletedImagePath(fileUrl)
77
- if(isNonWav)
78
- {
79
+ if (isNonWav) {
79
80
  File(wavPath).delete()
80
81
  }
81
82
  promise.resolve(returnableFilePath)
82
83
  } else {
83
- addLog("error: "+mp3Path)
84
+ addLog("error: " + mp3Path)
85
+ // Clean up the temp WAV on failure too; previously it was
86
+ // leaked because only the success path deleted it.
87
+ if (isNonWav) {
88
+ File(wavPath).delete()
89
+ }
84
90
  promise.resolve(_fileUrl)
85
91
  }
86
92
  }
87
93
  } catch (e: Exception) {
88
- promise.resolve(_fileUrl)
94
+ addLog("CompressAudio failed: " + e.localizedMessage)
95
+ // Clean up any temp WAV that was created before the failure.
96
+ val temp = wavPath
97
+ if (isNonWav && temp != null) {
98
+ try {
99
+ File(temp).delete()
100
+ } catch (_: Throwable) {
101
+ // ignore cleanup failures
102
+ }
103
+ }
104
+ promise.reject("audio_compress_failed", e.localizedMessage ?: "Audio compression failed", e)
89
105
  }
90
106
  }
91
107
 
@@ -101,35 +117,34 @@ class AudioCompressor {
101
117
  val options = AudioHelper.fromMap(optionMap)
102
118
  val quality = options.quality
103
119
 
104
- var isCompletedCallbackTriggered:Boolean=false
120
+ var isCompletedCallbackTriggered: Boolean = false
105
121
  try {
106
122
  var mp3Path = Utils.generateCacheFilePath("mp3", context)
107
123
  val input = File(fileUrl)
108
124
  val output = File(mp3Path)
109
125
 
110
126
  val CHUNK_SIZE = 8192
111
- addLog("Initialising wav reader")
127
+ addLog("Initialising wav reader")
112
128
 
113
- waveReader = WaveReader(input)
129
+ waveReader = WaveReader(input)
114
130
 
115
- try {
116
- waveReader!!.openWave()
117
- } catch (e: IOException) {
118
- e.printStackTrace()
119
- }
131
+ try {
132
+ waveReader!!.openWave()
133
+ } catch (e: IOException) {
134
+ addLog("openWave failed: " + e.localizedMessage)
135
+ completeCallback(e.localizedMessage ?: "Failed to open WAV", false)
136
+ return
137
+ }
120
138
 
121
- addLog("Intitialising encoder")
139
+ addLog("Intitialising encoder")
122
140
 
123
141
 
124
142
  // for bitrate
125
- var audioBitrate:Int
126
- if(options.bitrate != -1)
127
- {
128
- audioBitrate= options.bitrate/1000
129
- }
130
- else
131
- {
132
- audioBitrate=AudioHelper.getDestinationBitrateByQuality(actualFileUrl, quality!!)
143
+ var audioBitrate: Int
144
+ if (options.bitrate != -1) {
145
+ audioBitrate = options.bitrate / 1000
146
+ } else {
147
+ audioBitrate = AudioHelper.getDestinationBitrateByQuality(actualFileUrl, quality!!)
133
148
  Utils.addLog("dest bitrate: $audioBitrate")
134
149
  }
135
150
 
@@ -137,128 +152,126 @@ class AudioCompressor {
137
152
  androidLame.setOutBitrate(audioBitrate)
138
153
 
139
154
  // for channels
140
- var audioChannels:Int
141
- if(options.channels != -1){
142
- audioChannels= options.channels!!
143
- }
144
- else
145
- {
146
- audioChannels=waveReader!!.channels
155
+ var audioChannels: Int
156
+ if (options.channels != -1) {
157
+ audioChannels = options.channels!!
158
+ } else {
159
+ audioChannels = waveReader!!.channels
147
160
  }
148
161
  androidLame.setOutChannels(audioChannels)
149
162
 
150
163
  // for sample rate
151
164
  androidLame.setInSampleRate(waveReader!!.sampleRate)
152
- var audioSampleRate:Int
153
- if(options.samplerate != -1){
154
- audioSampleRate= options.samplerate!!
155
- }
156
- else
157
- {
158
- audioSampleRate=waveReader!!.sampleRate
165
+ var audioSampleRate: Int
166
+ if (options.samplerate != -1) {
167
+ audioSampleRate = options.samplerate!!
168
+ } else {
169
+ audioSampleRate = waveReader!!.sampleRate
159
170
  }
160
171
  androidLame.setOutSampleRate(audioSampleRate)
161
- val androidLameBuild=androidLame.build()
172
+ val androidLameBuild = androidLame.build()
162
173
 
163
- try {
164
- outputStream = BufferedOutputStream(FileOutputStream(output), OUTPUT_STREAM_BUFFER)
165
- } catch (e: FileNotFoundException) {
166
- e.printStackTrace()
167
- }
174
+ try {
175
+ outputStream = BufferedOutputStream(FileOutputStream(output), OUTPUT_STREAM_BUFFER)
176
+ } catch (e: FileNotFoundException) {
177
+ addLog("Failed to create output stream: " + e.localizedMessage)
178
+ completeCallback(e.localizedMessage ?: "Failed to create output file", false)
179
+ return
180
+ }
168
181
 
169
- var bytesRead = 0
182
+ var bytesRead = 0
170
183
 
171
- val buffer_l = ShortArray(CHUNK_SIZE)
172
- val buffer_r = ShortArray(CHUNK_SIZE)
173
- val mp3Buf = ByteArray(CHUNK_SIZE)
184
+ val buffer_l = ShortArray(CHUNK_SIZE)
185
+ val buffer_r = ShortArray(CHUNK_SIZE)
186
+ val mp3Buf = ByteArray(CHUNK_SIZE)
174
187
 
175
- val channels = waveReader!!.channels
188
+ val channels = waveReader!!.channels
176
189
 
177
- addLog("started encoding")
178
- while (true) {
179
- try {
180
- if (channels == 2) {
190
+ addLog("started encoding")
191
+ while (true) {
192
+ try {
193
+ if (channels == 2) {
194
+
195
+ bytesRead = waveReader!!.read(buffer_l, buffer_r, CHUNK_SIZE)
196
+ addLog("bytes read=$bytesRead")
181
197
 
182
- bytesRead = waveReader!!.read(buffer_l, buffer_r, CHUNK_SIZE)
183
- addLog("bytes read=$bytesRead")
198
+ if (bytesRead > 0) {
184
199
 
185
- if (bytesRead > 0) {
200
+ var bytesEncoded = 0
201
+ bytesEncoded = androidLameBuild.encode(buffer_l, buffer_r, bytesRead, mp3Buf)
202
+ addLog("bytes encoded=$bytesEncoded")
186
203
 
187
- var bytesEncoded = 0
188
- bytesEncoded = androidLameBuild.encode(buffer_l, buffer_r, bytesRead, mp3Buf)
189
- addLog("bytes encoded=$bytesEncoded")
204
+ if (bytesEncoded > 0) {
205
+ try {
206
+ addLog("writing mp3 buffer to outputstream with $bytesEncoded bytes")
207
+ outputStream!!.write(mp3Buf, 0, bytesEncoded)
208
+ } catch (e: IOException) {
209
+ e.printStackTrace()
210
+ }
190
211
 
191
- if (bytesEncoded > 0) {
192
- try {
193
- addLog("writing mp3 buffer to outputstream with $bytesEncoded bytes")
194
- outputStream!!.write(mp3Buf, 0, bytesEncoded)
195
- } catch (e: IOException) {
196
- e.printStackTrace()
197
212
  }
198
213
 
199
- }
214
+ } else
215
+ break
216
+ } else {
200
217
 
201
- } else
202
- break
203
- } else {
218
+ bytesRead = waveReader!!.read(buffer_l, CHUNK_SIZE)
219
+ addLog("bytes read=$bytesRead")
204
220
 
205
- bytesRead = waveReader!!.read(buffer_l, CHUNK_SIZE)
206
- addLog("bytes read=$bytesRead")
221
+ if (bytesRead > 0) {
222
+ var bytesEncoded = 0
207
223
 
208
- if (bytesRead > 0) {
209
- var bytesEncoded = 0
224
+ bytesEncoded = androidLameBuild.encode(buffer_l, buffer_l, bytesRead, mp3Buf)
225
+ addLog("bytes encoded=$bytesEncoded")
210
226
 
211
- bytesEncoded = androidLameBuild.encode(buffer_l, buffer_l, bytesRead, mp3Buf)
212
- addLog("bytes encoded=$bytesEncoded")
227
+ if (bytesEncoded > 0) {
228
+ try {
229
+ addLog("writing mp3 buffer to outputstream with $bytesEncoded bytes")
230
+ outputStream!!.write(mp3Buf, 0, bytesEncoded)
231
+ } catch (e: IOException) {
232
+ e.printStackTrace()
233
+ }
213
234
 
214
- if (bytesEncoded > 0) {
215
- try {
216
- addLog("writing mp3 buffer to outputstream with $bytesEncoded bytes")
217
- outputStream!!.write(mp3Buf, 0, bytesEncoded)
218
- } catch (e: IOException) {
219
- e.printStackTrace()
220
235
  }
221
236
 
222
- }
237
+ } else
238
+ break
239
+ }
223
240
 
224
- } else
225
- break
226
- }
227
241
 
242
+ } catch (e: IOException) {
243
+ e.printStackTrace()
244
+ }
228
245
 
229
- } catch (e: IOException) {
230
- e.printStackTrace()
231
246
  }
232
247
 
233
- }
234
-
235
- addLog("flushing final mp3buffer")
236
- val outputMp3buf = androidLameBuild.flush(mp3Buf)
237
- addLog("flushed $outputMp3buf bytes")
238
- if (outputMp3buf > 0) {
239
- try {
240
- addLog("writing final mp3buffer to outputstream")
241
- outputStream!!.write(mp3Buf, 0, outputMp3buf)
242
- addLog("closing output stream")
243
- outputStream!!.close()
244
- completeCallback(output.absolutePath, true)
245
- isCompletedCallbackTriggered=true
246
- } catch (e: IOException) {
247
- completeCallback(e.localizedMessage, false)
248
- e.printStackTrace()
248
+ addLog("flushing final mp3buffer")
249
+ val outputMp3buf = androidLameBuild.flush(mp3Buf)
250
+ addLog("flushed $outputMp3buf bytes")
251
+ if (outputMp3buf > 0) {
252
+ try {
253
+ addLog("writing final mp3buffer to outputstream")
254
+ outputStream!!.write(mp3Buf, 0, outputMp3buf)
255
+ addLog("closing output stream")
256
+ outputStream!!.close()
257
+ completeCallback(output.absolutePath, true)
258
+ isCompletedCallbackTriggered = true
259
+ } catch (e: IOException) {
260
+ completeCallback(e.localizedMessage, false)
261
+ isCompletedCallbackTriggered = true
262
+ e.printStackTrace()
263
+ }
249
264
  }
250
- }
251
265
 
252
266
  } catch (e: IOException) {
253
267
  completeCallback(e.localizedMessage, false)
268
+ isCompletedCallbackTriggered = true
254
269
  }
255
- if(!isCompletedCallbackTriggered)
256
- {
270
+ if (!isCompletedCallbackTriggered) {
257
271
  completeCallback("something went wrong", false)
258
272
  }
259
273
  }
260
274
 
261
275
 
262
-
263
276
  }
264
277
  }
@@ -0,0 +1,204 @@
1
+ package com.reactnativecompressor.Audio
2
+
3
+ import android.media.MediaCodec
4
+ import android.media.MediaExtractor
5
+ import android.media.MediaFormat
6
+ import com.reactnativecompressor.Utils.Utils.addLog
7
+ import java.io.BufferedOutputStream
8
+ import java.io.File
9
+ import java.io.FileOutputStream
10
+ import java.io.IOException
11
+ import java.io.RandomAccessFile
12
+ import java.nio.ByteBuffer
13
+ import java.nio.ByteOrder
14
+
15
+ /**
16
+ * Decodes any audio container/codec Android can play (m4a/AAC, mp3, ogg,
17
+ * flac, ...) into a 16-bit PCM little-endian WAV file that LAME can consume.
18
+ *
19
+ * LAME's WaveReader only accepts PCM 16-bit LE WAV, so for any compressed
20
+ * (or non-matching) input we must transcode through a real decoder — jlayer's
21
+ * Converter is MP3-only and silently fails for m4a, ogg, flac, etc.
22
+ */
23
+ internal object AudioTranscoder {
24
+
25
+ private const val TIMEOUT_US = 10_000L
26
+ private const val WAV_HEADER_SIZE = 44
27
+
28
+ /**
29
+ * Decode [srcPath] into a 16-bit PCM WAV at [dstPath].
30
+ *
31
+ * @throws IOException if the file cannot be opened, has no audio track,
32
+ * cannot be decoded on this device, or the output cannot be written.
33
+ */
34
+ @Throws(IOException::class)
35
+ fun decodeToWav(srcPath: String, dstPath: String) {
36
+ addLog("decodeToWav: $srcPath -> $dstPath")
37
+
38
+ val extractor = MediaExtractor()
39
+ var decoder: MediaCodec? = null
40
+ var output: BufferedOutputStream? = null
41
+
42
+ try {
43
+ // -- setup extractor -------------------------------------------------
44
+ extractor.setDataSource(srcPath)
45
+
46
+ val audioTrackIndex = findAudioTrackIndex(extractor)
47
+ ?: throw IOException("No audio track found in $srcPath")
48
+ extractor.selectTrack(audioTrackIndex)
49
+
50
+ val inputFormat = extractor.getTrackFormat(audioTrackIndex)
51
+ val mime = inputFormat.getString(MediaFormat.KEY_MIME)
52
+ ?: throw IOException("Audio track has no mime in $srcPath")
53
+ val sampleRate = if (inputFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE)) {
54
+ inputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
55
+ } else {
56
+ throw IOException("Audio track has no sample rate in $srcPath")
57
+ }
58
+ val channelCount = if (inputFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) {
59
+ inputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
60
+ } else {
61
+ throw IOException("Audio track has no channel count in $srcPath")
62
+ }
63
+
64
+ // -- create + configure decoder ---------------------------------------
65
+ try {
66
+ decoder = MediaCodec.createDecoderByType(mime)
67
+ } catch (e: IllegalArgumentException) {
68
+ throw IOException("Device cannot decode audio mime $mime", e)
69
+ } catch (e: Exception) {
70
+ throw IOException("Failed to create decoder for $mime", e)
71
+ }
72
+ decoder!!.configure(inputFormat, null, null, 0)
73
+ decoder!!.start()
74
+
75
+ // Capture a non-null val so Kotlin smart-cast works in the loop below.
76
+ val d = decoder!!
77
+
78
+ // -- prepare output WAV file ------------------------------------------
79
+ val outFile = File(dstPath)
80
+ outFile.parentFile?.mkdirs()
81
+ output = BufferedOutputStream(FileOutputStream(outFile))
82
+ // Reserve 44 bytes for the RIFF/WAVE header; we'll come back and
83
+ // overwrite with the correct sizes once we know how many PCM bytes
84
+ // were produced.
85
+ output.write(ByteArray(WAV_HEADER_SIZE))
86
+
87
+ var totalBytesWritten = 0L
88
+ val info = MediaCodec.BufferInfo()
89
+ var inputDone = false
90
+ var outputDone = false
91
+
92
+ // -- decode loop ------------------------------------------------------
93
+ while (!outputDone) {
94
+ // Feed compressed samples into the decoder.
95
+ if (!inputDone) {
96
+ val inIndex = d.dequeueInputBuffer(TIMEOUT_US)
97
+ if (inIndex >= 0) {
98
+ val inBuf = d.getInputBuffer(inIndex)
99
+ ?: throw IOException("Decoder returned null input buffer")
100
+ inBuf.clear()
101
+ val sampleSize = extractor.readSampleData(inBuf, 0)
102
+ if (sampleSize < 0) {
103
+ d.queueInputBuffer(
104
+ inIndex, 0, 0, 0,
105
+ MediaCodec.BUFFER_FLAG_END_OF_STREAM
106
+ )
107
+ inputDone = true
108
+ } else {
109
+ d.queueInputBuffer(
110
+ inIndex, 0, sampleSize, extractor.sampleTime, 0
111
+ )
112
+ extractor.advance()
113
+ }
114
+ }
115
+ }
116
+
117
+ // Drain decoded PCM and append it to the file (after the header
118
+ // placeholder).
119
+ val outIndex = d.dequeueOutputBuffer(info, TIMEOUT_US)
120
+ if (outIndex >= 0) {
121
+ val outBuf = d.getOutputBuffer(outIndex)
122
+ ?: throw IOException("Decoder returned null output buffer")
123
+ if (info.size > 0) {
124
+ // outBuf is a direct ByteBuffer; data is laid out in native byte
125
+ // order in [info.offset, info.offset + info.size). Copy the whole
126
+ // run in a single contiguous read.
127
+ val copy = ByteArray(info.size)
128
+ outBuf.position(info.offset)
129
+ outBuf.get(copy)
130
+ output.write(copy)
131
+ totalBytesWritten += info.size
132
+ }
133
+ d.releaseOutputBuffer(outIndex, false)
134
+
135
+ if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
136
+ outputDone = true
137
+ }
138
+ } else if (outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
139
+ // The final decoded format may differ from what we read at
140
+ // configure time (e.g. AAC decoder may change channel layout
141
+ // after the first frame). Log it for diagnostics; the header we
142
+ // write at the end will use the configure-time values, which is
143
+ // the standard PCM layout that LAME expects.
144
+ addLog("decoder output format changed: ${d.outputFormat}")
145
+ }
146
+ }
147
+
148
+ output.flush()
149
+
150
+ // Patch the WAV header with the final sizes now that we know them.
151
+ writeWavHeader(outFile, sampleRate, channelCount, totalBytesWritten)
152
+
153
+ addLog("decodeToWav: wrote $totalBytesWritten PCM bytes")
154
+ } finally {
155
+ runCatching { decoder?.stop() }
156
+ runCatching { decoder?.release() }
157
+ runCatching { extractor.release() }
158
+ runCatching { output?.close() }
159
+ }
160
+ }
161
+
162
+ private fun findAudioTrackIndex(extractor: MediaExtractor): Int? {
163
+ for (i in 0 until extractor.trackCount) {
164
+ val mime = extractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME)
165
+ if (mime != null && mime.startsWith("audio/")) return i
166
+ }
167
+ return null
168
+ }
169
+
170
+ /**
171
+ * Write a standard 44-byte PCM 16-bit little-endian WAV header into the
172
+ * first [WAV_HEADER_SIZE] bytes of [outFile], anchored to the [dataBytes]
173
+ * sample body that follows.
174
+ */
175
+ private fun writeWavHeader(outFile: File, sampleRate: Int, channels: Int, dataBytes: Long) {
176
+ val header = ByteBuffer.allocate(WAV_HEADER_SIZE).order(ByteOrder.LITTLE_ENDIAN)
177
+ val byteRate = sampleRate * channels * 2 // 16-bit = 2 bytes per sample
178
+ val blockAlign = (channels * 2).toShort()
179
+ val chunkSize = 36L + dataBytes
180
+
181
+ header.put("RIFF".toByteArray(Charsets.US_ASCII))
182
+ header.putInt(chunkSize.toInt())
183
+ header.put("WAVE".toByteArray(Charsets.US_ASCII))
184
+ header.put("fmt ".toByteArray(Charsets.US_ASCII))
185
+ header.putInt(16) // PCM fmt chunk size
186
+ header.putShort(1) // PCM format
187
+ header.putShort(channels.toShort())
188
+ header.putInt(sampleRate)
189
+ header.putInt(byteRate)
190
+ header.putShort(blockAlign)
191
+ header.putShort(16) // bits per sample
192
+ header.put("data".toByteArray(Charsets.US_ASCII))
193
+ header.putInt(dataBytes.toInt())
194
+
195
+ val raf = RandomAccessFile(outFile, "rw")
196
+ try {
197
+ raf.seek(0)
198
+ raf.write(header.array(), 0, WAV_HEADER_SIZE)
199
+ } finally {
200
+ raf.close()
201
+ }
202
+ }
203
+
204
+ }