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,29 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "AudioWaveform"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/maitrungduc1410/react-native-audio-waveform.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
+ s.private_header_files = "ios/**/*.h"
18
+
19
+ s.swift_versions = ["5.0"]
20
+ s.frameworks = "AVFoundation", "CoreMedia", "QuartzCore", "UIKit"
21
+
22
+ s.pod_target_xcconfig = {
23
+ "DEFINES_MODULE" => "YES",
24
+ "SWIFT_OBJC_INTERFACE_HEADER_NAME" => "AudioWaveform-Swift.h",
25
+ "CLANG_CXX_LANGUAGE_STANDARD" => "c++20"
26
+ }
27
+
28
+ install_modules_dependencies(s)
29
+ end
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Duc Trung Mai
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,296 @@
1
+ # react-native-waveform-player
2
+
3
+ Native audio-message UI for React Native — play any local or remote audio file
4
+ and render its waveform purely natively. Swift on iOS, Kotlin on Android,
5
+ Fabric (new architecture) only.
6
+
7
+ <p align="center">
8
+ <img src="./demo/ios.png" alt="iOS" width="48%" />
9
+ <img src="./demo/android.png" alt="Android" width="48%" />
10
+ </p>
11
+
12
+ ## Features
13
+
14
+ - Play / pause / scrub / cycle speed — all rendered in native code, no JS in
15
+ the hot path.
16
+ - Custom rounded-bar waveform with a partial-fill playhead (the bar straddling
17
+ the playhead is highlighted up to the exact pixel).
18
+ - Press-and-drag scrubbing with zero activation delay.
19
+ - Configurable bar size / gap / radius / count, played + unplayed colors.
20
+ - Built-in play button, time label (count-up or count-down), and tap-to-cycle
21
+ speed pill — each individually showable / themable.
22
+ - Pre-computed `samples` escape hatch when you already have peaks data.
23
+ - Controlled (`playing`, `speed`) and uncontrolled modes.
24
+ - Imperative `ref.play()` / `pause()` / `toggle()` / `seekTo(ms)` /
25
+ `setSpeed(s)`.
26
+ - Opt-in background playback via `playInBackground` (paused on backgrounding
27
+ by default), with `pauseUiUpdatesInBackground` to skip cheap-but-pointless
28
+ UI work while offscreen.
29
+ - Events: `onLoad`, `onLoadError`, `onPlayerStateChange`, `onTimeUpdate`,
30
+ `onSeek`, `onEnd`.
31
+
32
+ ## Installation
33
+
34
+ ```sh
35
+ npm install react-native-waveform-player
36
+ # or
37
+ yarn add react-native-waveform-player
38
+ ```
39
+
40
+ iOS:
41
+
42
+ ```sh
43
+ cd ios && pod install
44
+ ```
45
+
46
+ This library is Fabric-only; the host app must have the new architecture
47
+ enabled (it's the default in RN 0.85+).
48
+
49
+ ## Usage
50
+
51
+ ```tsx
52
+ import { AudioWaveformView } from 'react-native-waveform-player';
53
+
54
+ export function VoiceNote() {
55
+ return (
56
+ <AudioWaveformView
57
+ source={{
58
+ uri: 'https://example.com/voice-note.m4a',
59
+ }}
60
+ style={{ height: 56 }}
61
+ />
62
+ );
63
+ }
64
+ ```
65
+
66
+ ### Themed + count-down + custom speeds
67
+
68
+ ```tsx
69
+ <AudioWaveformView
70
+ source={{ uri: REMOTE_AUDIO }}
71
+ containerBackgroundColor="#0F172A"
72
+ containerBorderRadius={20}
73
+ playedBarColor="#22D3EE"
74
+ unplayedBarColor="rgba(34, 211, 238, 0.35)"
75
+ playButtonColor="#22D3EE"
76
+ timeColor="#A5F3FC"
77
+ timeMode="count-down"
78
+ speedColor="#0F172A"
79
+ speedBackgroundColor="#22D3EE"
80
+ speeds={[1, 1.5, 2]}
81
+ defaultSpeed={1.5}
82
+ barWidth={4}
83
+ barGap={3}
84
+ style={{ height: 56 }}
85
+ />
86
+ ```
87
+
88
+ ### Controlled component
89
+
90
+ When `playing` and/or `speed` are supplied, the component is fully controlled
91
+ — tapping the play button or speed pill fires `onPlayerStateChange` with the
92
+ *requested* new value but does **not** mutate internal state. Update the prop
93
+ in your parent state.
94
+
95
+ ```tsx
96
+ const [playing, setPlaying] = useState(false);
97
+ const [speed, setSpeed] = useState(1);
98
+
99
+ <AudioWaveformView
100
+ source={{ uri }}
101
+ playing={playing}
102
+ speed={speed}
103
+ onPlayerStateChange={(e) => {
104
+ if (e.isPlaying !== playing) setPlaying(e.isPlaying);
105
+ if (e.speed !== speed) setSpeed(e.speed);
106
+ }}
107
+ />;
108
+ ```
109
+
110
+ ### Imperative ref API
111
+
112
+ ```tsx
113
+ import {
114
+ AudioWaveformView,
115
+ type AudioWaveformViewRef,
116
+ } from 'react-native-waveform-player';
117
+
118
+ const ref = useRef<AudioWaveformViewRef>(null);
119
+
120
+ ref.current?.play();
121
+ ref.current?.pause();
122
+ ref.current?.toggle();
123
+ ref.current?.seekTo(0);
124
+ ref.current?.setSpeed(2);
125
+ ```
126
+
127
+ ### Pre-computed samples (skip native decode)
128
+
129
+ ```tsx
130
+ <AudioWaveformView
131
+ source={{ uri }}
132
+ samples={[0.1, 0.4, 0.85, 0.6, /* ... */]}
133
+ />
134
+ ```
135
+
136
+ ### Hide every chrome element (visualiser only)
137
+
138
+ ```tsx
139
+ <AudioWaveformView
140
+ source={{ uri }}
141
+ showPlayButton={false}
142
+ showTime={false}
143
+ showSpeedControl={false}
144
+ showBackground={false}
145
+ />
146
+ ```
147
+
148
+ ### Background playback
149
+
150
+ By default the component pauses playback when the host app is backgrounded
151
+ (matches iOS's default behaviour, and we add the same on Android for parity).
152
+ Opt in with `playInBackground`:
153
+
154
+ ```tsx
155
+ <AudioWaveformView source={{ uri }} playInBackground />
156
+ ```
157
+
158
+ When `playInBackground` is `true`:
159
+
160
+ #### iOS — required
161
+
162
+ Enable the **Audio** background mode on the host app target. Either:
163
+
164
+ 1. **Xcode** → Project → app target → **Signing & Capabilities** →
165
+ **+ Capability** → **Background Modes** → check
166
+ *Audio, AirPlay, and Picture in Picture*.
167
+
168
+ _or_
169
+
170
+ 2. Add to your app's `Info.plist`:
171
+
172
+ ```xml
173
+ <key>UIBackgroundModes</key>
174
+ <array>
175
+ <string>audio</string>
176
+ </array>
177
+ ```
178
+
179
+ The library configures `AVAudioSession` to `.playback` and activates it for
180
+ you. Note that this will play through the silent-mode switch and interrupt
181
+ other apps' audio (Spotify, etc.) by default. If your app already manages
182
+ its own audio session, set the category yourself before mounting the
183
+ component and the library won't override it.
184
+
185
+ #### Android — optional
186
+
187
+ `MediaPlayer` keeps playing through `Activity.onPause` already, so for
188
+ typical voice-message use cases **nothing extra is required**.
189
+
190
+ If you need playback to survive **device sleep** (screen off + idle, CPU
191
+ suspended), add `WAKE_LOCK` to your **app's** `AndroidManifest.xml`:
192
+
193
+ ```xml
194
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
195
+ ```
196
+
197
+ The library will then automatically call `MediaPlayer.setWakeMode` when
198
+ `playInBackground` is `true`. Without the permission `setWakeMode` is
199
+ silently skipped (a warning is logged) — playback still works while the
200
+ screen is on, it just pauses with the device.
201
+
202
+ #### Suspending UI work while backgrounded
203
+
204
+ The 30 Hz progress polling that drives the bars + time label keeps running
205
+ even after the OS has stopped compositing the view, so a tiny amount of
206
+ CPU is wasted on math + string formatting per tick.
207
+
208
+ `pauseUiUpdatesInBackground` (default `true`) gates that work:
209
+
210
+ - `true` — when backgrounded, skip the bars / time-label refreshes. The view
211
+ is offscreen so there's nothing visible to lose. The library snaps the UI
212
+ to the engine's current state on resume.
213
+ - `false` — keep refreshing in background (rare; only useful if something
214
+ in your view hierarchy is observing those UI changes from background).
215
+
216
+ `onTimeUpdate` keeps firing in either case, so Now Playing / Lock Screen /
217
+ analytics integrations work the same way.
218
+
219
+ ## Props
220
+
221
+ | Prop | Type | Default | Description |
222
+ | -------------------------- | ----------------------------------- | ------------------------- | ----------- |
223
+ | `source` (required) | `{ uri: string }` | — | Audio source. Supports `file://`, `https://`, `content://`. |
224
+ | `samples` | `number[]` | `undefined` | Pre-computed amplitudes in `[0, 1]`. When set, native decode is skipped. |
225
+ | `playedBarColor` | `ColorValue` | `#FFFFFF` | Color of the highlighted ("played") portion of each bar. |
226
+ | `unplayedBarColor` | `ColorValue` | `rgba(255,255,255,0.5)` | Color of the not-yet-played portion. |
227
+ | `barWidth` | `number` | `3` | Bar thickness in dp. |
228
+ | `barGap` | `number` | `2` | Gap between bars in dp. |
229
+ | `barRadius` | `number` | `barWidth / 2` | Bar corner radius in dp. |
230
+ | `barCount` | `number` | auto from view width | Force a specific number of bars. |
231
+ | `containerBackgroundColor` | `ColorValue` | `#3478F6` | Rounded container background. |
232
+ | `containerBorderRadius` | `number` | `16` | Rounded container corner radius. |
233
+ | `showBackground` | `boolean` | `true` | Whether to draw the rounded container background. |
234
+ | `showPlayButton` | `boolean` | `true` | |
235
+ | `playButtonColor` | `ColorValue` | `#FFFFFF` | Play / pause icon tint (uses SF Symbols on iOS, vector drawables on Android). |
236
+ | `showTime` | `boolean` | `true` | |
237
+ | `timeColor` | `ColorValue` | `#FFFFFF` | |
238
+ | `timeMode` | `'count-up' \| 'count-down'` | `'count-up'` | |
239
+ | `showSpeedControl` | `boolean` | `true` | |
240
+ | `speedColor` | `ColorValue` | `#FFFFFF` | Speed pill text color. |
241
+ | `speedBackgroundColor` | `ColorValue` | `rgba(255,255,255,0.25)` | Speed pill background color. |
242
+ | `speeds` | `number[]` | `[0.5, 1, 1.5, 2]` | Tap-to-cycle speed values. |
243
+ | `defaultSpeed` | `number` | `1` | Initial speed on mount. |
244
+ | `autoPlay` | `boolean` | `false` | Begin playback as soon as the source is ready. |
245
+ | `initialPositionMs` | `number` | `0` | Seek to this position (ms) on load. |
246
+ | `loop` | `boolean` | `false` | Restart from `0` on end-of-track. |
247
+ | `playInBackground` | `boolean` | `false` | Keep playing when the host app backgrounds. iOS requires the Audio Background Mode; Android optionally honours `WAKE_LOCK`. See [Background playback](#background-playback). |
248
+ | `pauseUiUpdatesInBackground` | `boolean` | `true` | While backgrounded, suspend the bars / time-label refreshes that piggy-back on every progress tick. The OS already skips painting; this saves the cheap math/string work. `onTimeUpdate` is unaffected. |
249
+ | `playing` | `boolean \| undefined` | `undefined` | Controlled playing state. When defined, internal play/pause taps are inert. |
250
+ | `speed` | `number \| undefined` | `undefined` | Controlled speed. When defined, internal speed-pill taps are inert. |
251
+
252
+ ### Events
253
+
254
+ | Event | Payload |
255
+ | ---------------------- | ---------------------------------------------------- |
256
+ | `onLoad` | `{ durationMs: number }` |
257
+ | `onLoadError` | `{ message: string }` |
258
+ | `onPlayerStateChange` | `{ state, isPlaying, speed, error? }` (full snapshot on every transition: load lifecycle, play/pause, speed change) |
259
+ | `onTimeUpdate` | `{ currentTimeMs, durationMs }` (≈30 Hz while playing) |
260
+ | `onSeek` | `{ positionMs }` (end of scrub gesture or `seekTo`) |
261
+ | `onEnd` | `{}` |
262
+
263
+ `state` is one of `'idle' | 'loading' | 'ready' | 'ended' | 'error'`.
264
+
265
+ ### Imperative API
266
+
267
+ ```ts
268
+ type AudioWaveformViewRef = {
269
+ play: () => void;
270
+ pause: () => void;
271
+ toggle: () => void;
272
+ seekTo: (positionMs: number) => void;
273
+ setSpeed: (speed: number) => void;
274
+ };
275
+ ```
276
+
277
+ ## Out of scope
278
+
279
+ - Recording (playback + visualisation only).
280
+ - Live / streaming waveforms — we visualise a fixed audio file.
281
+ - `react-native-gesture-handler` / Reanimated integration — gestures are
282
+ handled natively for zero JS overhead.
283
+
284
+ ## Contributing
285
+
286
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
287
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
288
+ - [Code of conduct](CODE_OF_CONDUCT.md)
289
+
290
+ ## License
291
+
292
+ MIT
293
+
294
+ ---
295
+
296
+ Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
@@ -0,0 +1,67 @@
1
+ buildscript {
2
+ ext.AudioWaveform = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36,
6
+ targetSdkVersion: 36
7
+ ]
8
+
9
+ ext.getExtOrDefault = { prop ->
10
+ if (rootProject.ext.has(prop)) {
11
+ return rootProject.ext.get(prop)
12
+ }
13
+
14
+ return AudioWaveform[prop]
15
+ }
16
+
17
+ repositories {
18
+ google()
19
+ mavenCentral()
20
+ }
21
+
22
+ dependencies {
23
+ classpath "com.android.tools.build:gradle:8.7.2"
24
+ // noinspection DifferentKotlinGradleVersion
25
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
26
+ }
27
+ }
28
+
29
+
30
+ apply plugin: "com.android.library"
31
+ apply plugin: "kotlin-android"
32
+
33
+ apply plugin: "com.facebook.react"
34
+
35
+ android {
36
+ namespace "com.audiowaveform"
37
+
38
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
39
+
40
+ defaultConfig {
41
+ minSdkVersion getExtOrDefault("minSdkVersion")
42
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
43
+ }
44
+
45
+ buildFeatures {
46
+ buildConfig true
47
+ }
48
+
49
+ buildTypes {
50
+ release {
51
+ minifyEnabled false
52
+ }
53
+ }
54
+
55
+ lint {
56
+ disable "GradleCompatible"
57
+ }
58
+
59
+ compileOptions {
60
+ sourceCompatibility JavaVersion.VERSION_1_8
61
+ targetCompatibility JavaVersion.VERSION_1_8
62
+ }
63
+ }
64
+
65
+ dependencies {
66
+ implementation "com.facebook.react:react-android"
67
+ }
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.INTERNET" />
3
+ </manifest>