react-native-waveform-player 0.0.1 → 1.0.0

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.
Files changed (46) hide show
  1. package/AudioWaveform.podspec +29 -0
  2. package/LICENSE +20 -0
  3. package/README.md +296 -0
  4. package/android/build.gradle +67 -0
  5. package/android/src/main/AndroidManifest.xml +3 -0
  6. package/android/src/main/java/com/audiowaveform/AudioPlayerEngine.kt +353 -0
  7. package/android/src/main/java/com/audiowaveform/AudioWaveformEvent.kt +22 -0
  8. package/android/src/main/java/com/audiowaveform/AudioWaveformPackage.kt +17 -0
  9. package/android/src/main/java/com/audiowaveform/AudioWaveformView.kt +715 -0
  10. package/android/src/main/java/com/audiowaveform/AudioWaveformViewManager.kt +234 -0
  11. package/android/src/main/java/com/audiowaveform/PlayPauseButton.kt +106 -0
  12. package/android/src/main/java/com/audiowaveform/SpeedPillView.kt +70 -0
  13. package/android/src/main/java/com/audiowaveform/WaveformBarsView.kt +358 -0
  14. package/android/src/main/java/com/audiowaveform/WaveformDecoder.kt +240 -0
  15. package/android/src/main/res/drawable/pause_fill.xml +15 -0
  16. package/android/src/main/res/drawable/play_fill.xml +15 -0
  17. package/ios/AudioPlayerEngine.swift +281 -0
  18. package/ios/AudioWaveformView.h +14 -0
  19. package/ios/AudioWaveformView.mm +307 -0
  20. package/ios/AudioWaveformViewImpl.swift +835 -0
  21. package/ios/PlayPauseButton.swift +118 -0
  22. package/ios/SpeedPillView.swift +70 -0
  23. package/ios/WaveformBarsView.swift +327 -0
  24. package/ios/WaveformDecoder.swift +332 -0
  25. package/lib/module/AudioWaveformView.js +8 -0
  26. package/lib/module/AudioWaveformView.js.map +1 -0
  27. package/lib/module/AudioWaveformView.native.js +79 -0
  28. package/lib/module/AudioWaveformView.native.js.map +1 -0
  29. package/lib/module/AudioWaveformViewNativeComponent.ts +95 -0
  30. package/lib/module/index.js +4 -0
  31. package/lib/module/index.js.map +1 -0
  32. package/lib/module/package.json +1 -0
  33. package/lib/typescript/package.json +1 -0
  34. package/lib/typescript/src/AudioWaveformView.d.ts +233 -0
  35. package/lib/typescript/src/AudioWaveformView.d.ts.map +1 -0
  36. package/lib/typescript/src/AudioWaveformView.native.d.ts +335 -0
  37. package/lib/typescript/src/AudioWaveformView.native.d.ts.map +1 -0
  38. package/lib/typescript/src/AudioWaveformViewNativeComponent.d.ts +71 -0
  39. package/lib/typescript/src/AudioWaveformViewNativeComponent.d.ts.map +1 -0
  40. package/lib/typescript/src/index.d.ts +3 -0
  41. package/lib/typescript/src/index.d.ts.map +1 -0
  42. package/package.json +138 -7
  43. package/src/AudioWaveformView.native.tsx +281 -0
  44. package/src/AudioWaveformView.tsx +96 -0
  45. package/src/AudioWaveformViewNativeComponent.ts +95 -0
  46. package/src/index.tsx +13 -0
@@ -0,0 +1,234 @@
1
+ package com.audiowaveform
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.ReactApplicationContext
5
+ import com.facebook.react.bridge.ReadableArray
6
+ import com.facebook.react.bridge.ReadableMap
7
+ import com.facebook.react.bridge.WritableMap
8
+ import com.facebook.react.module.annotations.ReactModule
9
+ import com.facebook.react.uimanager.SimpleViewManager
10
+ import com.facebook.react.uimanager.ThemedReactContext
11
+ import com.facebook.react.uimanager.UIManagerHelper
12
+ import com.facebook.react.uimanager.ViewManagerDelegate
13
+ import com.facebook.react.uimanager.events.EventDispatcher
14
+ import com.facebook.react.viewmanagers.AudioWaveformViewManagerDelegate
15
+ import com.facebook.react.viewmanagers.AudioWaveformViewManagerInterface
16
+
17
+ @ReactModule(name = AudioWaveformViewManager.NAME)
18
+ class AudioWaveformViewManager(@Suppress("UNUSED_PARAMETER") context: ReactApplicationContext) :
19
+ SimpleViewManager<AudioWaveformView>(),
20
+ AudioWaveformViewManagerInterface<AudioWaveformView> {
21
+
22
+ private val mDelegate: ViewManagerDelegate<AudioWaveformView> =
23
+ AudioWaveformViewManagerDelegate(this)
24
+
25
+ override fun getDelegate(): ViewManagerDelegate<AudioWaveformView> = mDelegate
26
+
27
+ override fun getName(): String = NAME
28
+
29
+ public override fun createViewInstance(context: ThemedReactContext): AudioWaveformView {
30
+ val view = AudioWaveformView(context)
31
+ wireEvents(view)
32
+ return view
33
+ }
34
+
35
+ /** Hook the AudioWaveformView's callback closures up to the Fabric event dispatcher. */
36
+ private fun wireEvents(view: AudioWaveformView) {
37
+ view.onLoad = { durationMs ->
38
+ dispatchEvent(view, "topLoad") { putInt("durationMs", durationMs) }
39
+ }
40
+ view.onLoadError = { message ->
41
+ dispatchEvent(view, "topLoadError") { putString("message", message) }
42
+ }
43
+ view.onPlayerStateChange = { state, isPlaying, speed, error ->
44
+ dispatchEvent(view, "topPlayerStateChange") {
45
+ putString("state", state)
46
+ putBoolean("isPlaying", isPlaying)
47
+ putDouble("speed", speed.toDouble())
48
+ putString("error", error)
49
+ }
50
+ }
51
+ view.onTimeUpdate = { currentMs, durationMs ->
52
+ dispatchEvent(view, "topTimeUpdate") {
53
+ putInt("currentTimeMs", currentMs)
54
+ putInt("durationMs", durationMs)
55
+ }
56
+ }
57
+ view.onSeek = { positionMs ->
58
+ dispatchEvent(view, "topSeek") { putInt("positionMs", positionMs) }
59
+ }
60
+ view.onEnd = {
61
+ dispatchEvent(view, "topEnd") {}
62
+ }
63
+ }
64
+
65
+ private inline fun dispatchEvent(
66
+ view: AudioWaveformView,
67
+ eventName: String,
68
+ builder: WritableMap.() -> Unit
69
+ ) {
70
+ val context = view.context as? ThemedReactContext ?: return
71
+ val dispatcher: EventDispatcher? = UIManagerHelper.getEventDispatcherForReactTag(
72
+ context,
73
+ view.id
74
+ )
75
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
76
+ val payload = Arguments.createMap()
77
+ payload.builder()
78
+ dispatcher?.dispatchEvent(
79
+ AudioWaveformEvent(surfaceId, view.id, eventName, payload)
80
+ )
81
+ }
82
+
83
+ // region Fabric prop setters (codegen interface) ----------------------------
84
+
85
+ override fun setSource(view: AudioWaveformView, value: ReadableMap?) {
86
+ val uri = value?.getString("uri") ?: ""
87
+ view.sourceUri = uri
88
+ }
89
+
90
+ override fun setSamples(view: AudioWaveformView, value: ReadableArray?) {
91
+ view.setSamplesFromArray(value)
92
+ }
93
+
94
+ override fun setPlayedBarColor(view: AudioWaveformView, value: Int?) {
95
+ view.playedBarColor = value ?: android.graphics.Color.WHITE
96
+ }
97
+
98
+ override fun setUnplayedBarColor(view: AudioWaveformView, value: Int?) {
99
+ view.unplayedBarColor =
100
+ value ?: android.graphics.Color.argb(128, 255, 255, 255)
101
+ }
102
+
103
+ override fun setBarWidth(view: AudioWaveformView, value: Float) {
104
+ view.barWidthDp = if (value > 0) value else 3f
105
+ }
106
+
107
+ override fun setBarGap(view: AudioWaveformView, value: Float) {
108
+ view.barGapDp = if (value >= 0) value else 2f
109
+ }
110
+
111
+ override fun setBarRadius(view: AudioWaveformView, value: Float) {
112
+ view.barRadiusDp = value // -1 (or any negative) means "auto" = barWidth / 2
113
+ }
114
+
115
+ override fun setBarCount(view: AudioWaveformView, value: Int) {
116
+ view.barCountOverride = value.coerceAtLeast(0)
117
+ }
118
+
119
+ override fun setContainerBackgroundColor(view: AudioWaveformView, value: Int?) {
120
+ view.containerBackgroundColor =
121
+ value ?: android.graphics.Color.parseColor("#3478F6")
122
+ }
123
+
124
+ override fun setContainerBorderRadius(view: AudioWaveformView, value: Float) {
125
+ view.containerBorderRadiusDp = if (value >= 0) value else 16f
126
+ }
127
+
128
+ override fun setShowBackground(view: AudioWaveformView, value: Boolean) {
129
+ view.showBackground = value
130
+ }
131
+
132
+ override fun setShowPlayButton(view: AudioWaveformView, value: Boolean) {
133
+ view.showPlayButton = value
134
+ }
135
+
136
+ override fun setPlayButtonColor(view: AudioWaveformView, value: Int?) {
137
+ view.playButtonColor = value ?: android.graphics.Color.WHITE
138
+ }
139
+
140
+ override fun setShowTime(view: AudioWaveformView, value: Boolean) {
141
+ view.showTime = value
142
+ }
143
+
144
+ override fun setTimeColor(view: AudioWaveformView, value: Int?) {
145
+ view.timeColor = value ?: android.graphics.Color.WHITE
146
+ }
147
+
148
+ override fun setTimeMode(view: AudioWaveformView, value: String?) {
149
+ view.timeMode = value ?: "count-up"
150
+ }
151
+
152
+ override fun setShowSpeedControl(view: AudioWaveformView, value: Boolean) {
153
+ view.showSpeedControl = value
154
+ }
155
+
156
+ override fun setSpeedColor(view: AudioWaveformView, value: Int?) {
157
+ view.speedColor = value ?: android.graphics.Color.WHITE
158
+ }
159
+
160
+ override fun setSpeedBackgroundColor(view: AudioWaveformView, value: Int?) {
161
+ view.speedBackgroundColor =
162
+ value ?: android.graphics.Color.argb(64, 255, 255, 255)
163
+ }
164
+
165
+ override fun setSpeeds(view: AudioWaveformView, value: ReadableArray?) {
166
+ view.setSpeedsFromArray(value)
167
+ }
168
+
169
+ override fun setDefaultSpeed(view: AudioWaveformView, value: Float) {
170
+ view.defaultSpeed = if (value > 0) value else 1f
171
+ }
172
+
173
+ override fun setAutoPlay(view: AudioWaveformView, value: Boolean) {
174
+ view.autoPlay = value
175
+ }
176
+
177
+ override fun setInitialPositionMs(view: AudioWaveformView, value: Int) {
178
+ view.initialPositionMs = value.coerceAtLeast(0)
179
+ }
180
+
181
+ override fun setLoop(view: AudioWaveformView, value: Boolean) {
182
+ view.loopPlayback = value
183
+ }
184
+
185
+ override fun setPlayInBackground(view: AudioWaveformView, value: Boolean) {
186
+ view.playInBackground = value
187
+ }
188
+
189
+ override fun setPauseUiUpdatesInBackground(view: AudioWaveformView, value: Boolean) {
190
+ view.pauseUiUpdatesInBackground = value
191
+ }
192
+
193
+ override fun setControlledPlaying(view: AudioWaveformView, value: Int) {
194
+ view.controlledPlaying = value
195
+ }
196
+
197
+ override fun setControlledSpeed(view: AudioWaveformView, value: Float) {
198
+ view.controlledSpeed = value
199
+ }
200
+
201
+ // endregion
202
+
203
+ // region Commands -----------------------------------------------------------
204
+
205
+ override fun play(view: AudioWaveformView) {
206
+ view.play()
207
+ }
208
+
209
+ override fun pause(view: AudioWaveformView) {
210
+ view.pause()
211
+ }
212
+
213
+ override fun toggle(view: AudioWaveformView) {
214
+ view.toggle()
215
+ }
216
+
217
+ override fun seekTo(view: AudioWaveformView, positionMs: Int) {
218
+ view.seekTo(positionMs)
219
+ }
220
+
221
+ override fun setSpeed(view: AudioWaveformView, speed: Float) {
222
+ view.setSpeedValue(speed)
223
+ }
224
+
225
+ // endregion
226
+
227
+ override fun onDropViewInstance(view: AudioWaveformView) {
228
+ super.onDropViewInstance(view)
229
+ }
230
+
231
+ companion object {
232
+ const val NAME = "AudioWaveformView"
233
+ }
234
+ }
@@ -0,0 +1,106 @@
1
+ package com.audiowaveform
2
+
3
+ import android.content.Context
4
+ import android.content.res.ColorStateList
5
+ import android.graphics.Color
6
+ import android.util.AttributeSet
7
+ import android.view.Gravity
8
+ import android.view.View
9
+ import android.widget.FrameLayout
10
+ import android.widget.ImageView
11
+ import android.widget.ProgressBar
12
+
13
+ /**
14
+ * Play/pause button backed by the bundled vector drawables
15
+ * `R.drawable.play_fill` and `R.drawable.pause_fill`. Tintable via `iconColor`.
16
+ *
17
+ * While `isLoading` is `true`, the icon is hidden and a native
18
+ * indeterminate `ProgressBar` is shown in its place. The view stays
19
+ * clickable so callers can queue a "play once ready" intent.
20
+ */
21
+ class PlayPauseButton @JvmOverloads constructor(
22
+ context: Context,
23
+ attrs: AttributeSet? = null,
24
+ defStyleAttr: Int = 0
25
+ ) : FrameLayout(context, attrs, defStyleAttr) {
26
+
27
+ var isPlaying: Boolean = false
28
+ set(value) {
29
+ if (field == value) return
30
+ field = value
31
+ updateImage()
32
+ }
33
+
34
+ var isLoading: Boolean = false
35
+ set(value) {
36
+ if (field == value) return
37
+ field = value
38
+ updateLoadingState()
39
+ }
40
+
41
+ var iconColor: Int = Color.WHITE
42
+ set(value) {
43
+ field = value
44
+ imageView.setColorFilter(value)
45
+ applySpinnerTint(value)
46
+ }
47
+
48
+ private val imageView = ImageView(context).apply {
49
+ scaleType = ImageView.ScaleType.FIT_CENTER
50
+ setColorFilter(Color.WHITE)
51
+ }
52
+
53
+ private val spinner = ProgressBar(context).apply {
54
+ isIndeterminate = true
55
+ visibility = View.GONE
56
+ }
57
+
58
+ init {
59
+ addView(
60
+ imageView,
61
+ LayoutParams(
62
+ LayoutParams.MATCH_PARENT,
63
+ LayoutParams.MATCH_PARENT
64
+ )
65
+ )
66
+ // Spinner sits on top of the icon (which is hidden while loading).
67
+ // Wrap content + center so the OS-default size is preserved.
68
+ addView(
69
+ spinner,
70
+ LayoutParams(
71
+ LayoutParams.WRAP_CONTENT,
72
+ LayoutParams.WRAP_CONTENT,
73
+ Gravity.CENTER
74
+ )
75
+ )
76
+ isClickable = true
77
+ isFocusable = true
78
+ applySpinnerTint(iconColor)
79
+ updateImage()
80
+ }
81
+
82
+ private fun updateImage() {
83
+ val res = if (isPlaying) R.drawable.pause_fill else R.drawable.play_fill
84
+ imageView.setImageResource(res)
85
+ imageView.setColorFilter(iconColor)
86
+ }
87
+
88
+ private fun updateLoadingState() {
89
+ if (isLoading) {
90
+ imageView.visibility = View.INVISIBLE
91
+ spinner.visibility = View.VISIBLE
92
+ } else {
93
+ imageView.visibility = View.VISIBLE
94
+ spinner.visibility = View.GONE
95
+ }
96
+ }
97
+
98
+ private fun applySpinnerTint(color: Int) {
99
+ spinner.indeterminateTintList = ColorStateList.valueOf(color)
100
+ }
101
+
102
+ override fun performClick(): Boolean {
103
+ super.performClick()
104
+ return true
105
+ }
106
+ }
@@ -0,0 +1,70 @@
1
+ package com.audiowaveform
2
+
3
+ import android.content.Context
4
+ import android.graphics.Color
5
+ import android.graphics.Typeface
6
+ import android.graphics.drawable.GradientDrawable
7
+ import android.util.AttributeSet
8
+ import android.view.Gravity
9
+ import android.widget.TextView
10
+ import kotlin.math.floor
11
+
12
+ /**
13
+ * Rounded "1.5x" speed-rate label. Tap-to-cycle is wired up via `onTap`;
14
+ * the parent (`AudioWaveformView`) is responsible for applying the new rate.
15
+ */
16
+ class SpeedPillView @JvmOverloads constructor(
17
+ context: Context,
18
+ attrs: AttributeSet? = null,
19
+ defStyleAttr: Int = 0
20
+ ) : TextView(context, attrs, defStyleAttr) {
21
+
22
+ private val pillBackground = GradientDrawable().apply {
23
+ shape = GradientDrawable.RECTANGLE
24
+ setColor(Color.argb(64, 255, 255, 255))
25
+ }
26
+
27
+ var pillColor: Int = Color.argb(64, 255, 255, 255)
28
+ set(value) {
29
+ field = value
30
+ pillBackground.setColor(value)
31
+ invalidate()
32
+ }
33
+
34
+ var onTap: (() -> Unit)? = null
35
+
36
+ init {
37
+ gravity = Gravity.CENTER
38
+ setTextColor(Color.WHITE)
39
+ textSize = 12f
40
+ setTypeface(typeface, Typeface.BOLD)
41
+ background = pillBackground
42
+ // Vertical padding stays small; the pill height is driven by intrinsic size.
43
+ val hPadding = (8f * resources.displayMetrics.density).toInt()
44
+ val vPadding = (2f * resources.displayMetrics.density).toInt()
45
+ setPadding(hPadding, vPadding, hPadding, vPadding)
46
+ isClickable = true
47
+ isFocusable = true
48
+ setOnClickListener {
49
+ animate().alpha(0.6f).setDuration(80).withEndAction {
50
+ animate().alpha(1.0f).setDuration(120).start()
51
+ }.start()
52
+ onTap?.invoke()
53
+ }
54
+ }
55
+
56
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
57
+ super.onSizeChanged(w, h, oldw, oldh)
58
+ // Make the corner radius track the height for a perfect pill shape.
59
+ pillBackground.cornerRadius = h / 2f
60
+ }
61
+
62
+ fun setSpeed(speed: Float) {
63
+ val rounded = (speed * 10f).toInt().toFloat() / 10f
64
+ text = if (rounded == floor(rounded)) {
65
+ "${rounded.toInt()}x"
66
+ } else {
67
+ String.format("%.1fx", rounded)
68
+ }
69
+ }
70
+ }