Inspiration
I used to play the same ambient recordings to relax. After listening to them enough times, I began to remember which sound came next. That anticipation made it harder to rest. I wanted to make a soundscape I could return to without memorizing it, and a screen that felt calming even before playback began.
What it does

Ambient generates sounds such as rain, waves, and wind in real time. Their patterns and small details keep changing as you listen. Flowing ink responds to the sound. When you first open the app, you see a single play arrow. Tap it, and the arrow dissolves into the ink.
Because Ambient is designed for background listening, I wanted people to enjoy it on the devices they already use for work and relaxation. I also released it on TVs, smartwatches, and desktop computers.

I also wanted people to be able to listen to Ambient in the background while working in VR. Since there’s still little competition in the VR space, I think this would be an effective marketing strategy. For that reason, the app is now compatible with Vision Pro, which has been confirmed to support background playback. As of September 30, 2026, Ambient appeared first in search results for "ambient" in the Vision Pro App Store in both the US and Japan.
On Vision Pro, I use particle visuals instead of ink to make use of the surrounding space. On Apple Watch, I use a lighter particle renderer to help conserve battery power.
The app has no ads. Free listening lasts up to 60 minutes per session. Premium adds unlimited listening and background playback.
How we built it
Cross-platform architecture

KMP let me develop sound generation, scene transitions, playback state, and listening limits in one shared core. With separate implementations for all eight platform families, each core change would need equivalent regression checks in eight codebases. Instead, I maintain one implementation and one shared test suite, then validate each platform's audio, UI, and lifecycle integration.
Android calls Kotlin directly, Swift imports a generated Kotlin/Native framework, desktop apps use a C interface, and the web runs Kotlin/JS inside an AudioWorklet. These adapters connect each app's controls to the same engine and return shared state, so I can keep each platform's own interface without reimplementing the playback rules.
| Platform | Shared engine bridge | Interface, audio, visuals |
|---|---|---|
| iOS / iPadOS | Kotlin/Native framework | SwiftUI, AVAudioEngine, Metal |
| Android / Android TV | Kotlin/JVM module | Android Views, AudioTrack, Vulkan or OpenGL ES |
| watchOS | Kotlin/Native framework | SwiftUI, AVAudioEngine, Canvas particles |
| visionOS | Custom Kotlin/Native target | SwiftUI, AVAudioEngine, RealityKit and Metal particles |
| macOS | Kotlin/Native C bridge | SwiftUI, AVAudioEngine, Metal |
| Windows | Kotlin/Native C bridge | Win32, WASAPI, Vulkan |
| Linux | Kotlin/Native C bridge | GTK4, ALSA, Vulkan |
| Web | Kotlin/JS in AudioWorklet | HTML controls, Web Audio, WebGPU |
iPadOS uses the iOS app, and Android TV uses the Android APK with support for TV remote input. Web playback controls remain available without WebGPU.
The ink simulation builds on Stable Fluids with custom color mixing.
I extracted Ambient's reusable audio playback layer into KMP Procedural Audio, a lightweight Kotlin Multiplatform library released under the MIT license. It offers shared playback controls and source switching across Android, iOS, macOS, watchOS, Windows, Linux, and the web.
Premium across devices
Premium purchased in the iOS or Google Play Android app also works on Windows, Linux, and the web. The receiving device displays a QR code. The user scans it in the mobile app, checks the device name, and approves the link, without creating a separate account.
Kotlin Multiplatform shares the logic behind this flow. The same core manages pairing, waiting for approval, saved access, expiry, and removing linked devices. Each app connects it to its own interface, networking, credential storage, and store purchase SDK. This keeps the linking rules consistent while allowing each platform to use its native capabilities.
A Cloudflare Workers service verifies the StoreKit transaction or Google Play purchase token and checks that it matches an active Premium purchase in RevenueCat. D1 stores the registrations and allows up to three linked devices or browser profiles per eligible purchase. The QR code is valid for five minutes and contains an approval code, with a separate token used by the receiving device to check its access.
Previously verified Premium remains available offline only until the server's access deadline. Expired access or a check that reports Premium is no longer active returns playback to the free tier’s 60-minute session limit. Users can remove links from the mobile app. Network and store checks run separately from sound generation.

Read more about the shared engine, fluid visuals, and visionOS support in my dev.to article.
Challenges we ran into
Kotlin Multiplatform does not officially support visionOS. I forked Kotlin and added the target needed to build Ambient for Vision Pro. While researching how to contribute that work, I found KT-59571, the existing visionOS support request. Its earlier LLVM limitation had been resolved in the current Kotlin/Native version.
I refined my implementation and submitted draft PR #7797. JetBrains explained that adding an official target would increase CI and maintenance costs, so the PR was closed without being merged. I appreciated the care they took in explaining the decision.
The biggest implementation challenge was optimizing performance. Both audio and graphics required real-time processing, so I had to decide which tasks should run on the CPU and which on the GPU. Audio was especially sensitive to timing: missing an audio-buffer deadline could cause clicks or dropouts.
Physically faithful color mixing quickly made the ink look muddy. I chose a stylized color model that keeps the blends soft and visually distinct rather than strictly realistic.
Accomplishments that we're proud of
One shared core now supports builds across eight platform families, from phones and TVs to desktops, the web, Apple Watch, and Vision Pro. I am especially proud that the Vision Pro app uses the shared Kotlin core and lets people purchase Premium in the app, with purchases powered by RevenueCat.
While the “Ship Kotlin Everywhere” category does not prioritize the number of supported platforms, reaching users through multiple app stores is an important part of Ambient’s strategy. Kotlin Multiplatform makes this possible while helping me deliver a consistent experience across devices.
What we learned
I chose an affordable monthly subscription without a free trial and offered a lifetime purchase alongside it. My hope was that the monthly price would make Premium approachable while giving people a clear alternative if they preferred to pay once. The first two paying customers bought lifetime access without first subscribing to the monthly plan.
The visionOS work also taught me that getting a target to build is only one part of official support. CI, libraries, distribution, and long-term maintenance matter too.
What's next for ambient
I plan to improve the quality of Ambient’s 50 sounds and add more variety. I also want to make the ink more relaxing to watch by refining how long it stays on screen and how its colors blend. There is still room to make both the audio and the visuals feel more natural and inviting.
I also plan to bring Ambient to Apple TV, Wear OS and Meta Quest. I couldn’t add support for these platforms during Shipaton. What matters to me is how they fit into people’s lives. Someone who enjoys Ambient on their phone should also be able to play it on their TV while relaxing at home. I want to help people discover these options and give them a consistent experience across the devices they use.
The market for ambient sounds and sleep music is crowded. I want to establish strong user retention before investing in paid advertising, so I plan to focus on ASO first. Ambient’s broad platform support is a key part of that strategy. Each store gives people another way to discover the app. I’ll tailor each listing to its platform and highlight how people can use Ambient on their other devices. The goal is not just to increase downloads but to help people make Ambient part of their routine. I’ll track retention and Premium purchases across stores to guide further development and decide when to start testing paid advertising.
ASO takes time to produce results. One reason I offered a lifetime plan alongside the monthly subscription was to generate revenue during Shipaton. The lifetime plan has brought in revenue and met that short-term goal.
With that short-term goal met, my next focus is long-term revenue. If retention data indicates that focusing on subscriptions would increase LTV, I’ll stop offering the lifetime plan to new users. As part of that shift, I’ll adjust the monthly subscription price and introduce an annual plan. Once I have enough evidence that the expected revenue per customer justifies the acquisition cost, I’ll start testing paid advertising to complement organic growth.
JetBrains also pointed me to KT-43974, a request to allow Kotlin targets to be maintained outside the main repository. I hope to contribute the visionOS support I developed for Ambient if that becomes possible.
Built With
- c++
- cloudflare
- kmp
- kotlin
- metal
- opengles
- revenuecat
- swift
- typescript
- vulkan
- webgpu
Log in or sign up for Devpost to join the conversation.