Skip to content
SkyliveLLCPublic

About

Waze for noise: a crowdsourced sound map, starting in San Francisco

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Soundmap

A crowdsourced map of how loud places are, across the United States. Think Waze, but for noise. It helps remote workers find a quiet café and helps people with sensory sensitivities plan calmer routes.

Status: early prototype. iOS and Android, built with Expo.

What works today

  • Measure. A 30 second, A-weighted noise measurement on your phone. It reports the average (LAeq), background (L90), loud moments (L10), peak (LAmax), and spikiness (L10 − L90).
  • Map. Add a measurement to the shared map and it shows up as a colored hexagon (H3, resolution 10, about one city block) for everyone, live. Hexagons with several measurements show their energy average. Tap any block to see its reading. A measurement added offline waits on the phone and uploads when the connection returns. The map opens where you are once you've allowed location, and on the lower 48 before that.
  • Street noise. All 50 states and DC start colored with transportation noise (roads, rail and aviation) from the US DOT National Transportation Noise Map (NTNM). It is a modeled 24 hour average, not a measurement, so the app shows it faintly under your hexagons and labels it as modeled. It is never averaged into your readings. Zoomed in, each hexagon is a block. Zoomed out, larger hexagons show the area's average, with blocks quieter than 45 dBA counted as silent.
  • Quiet walks. Long-press the map where you're going, then start from your location or a second long-press. The app plans the walk on the phone and shows a route that is measurably quieter than the shortest one, colored block by block, with the extra distance and time. Measured blocks count first and modeled street noise fills in elsewhere. Blocks DOT doesn't model, like a pier, are drawn grey and left out of how much quieter a route is. Walks up to 5 km, in California for now.
  • Venues. When you add a measurement near cafés, libraries or coworking spaces, the app asks whether you were in one of them. Its dot on the map opens a card with its loudness by hour of day, so you can see when it is quietest. Places come from OpenStreetMap.

Phone microphones vary, so each reading is converted to dB SPL with an offset for that phone model and stored with the model and calibration it used. Models are added to CALIBRATIONS in src/lib/calibration.ts once measured against a reference sound level meter. Until a model has an entry, its readings use a placeholder offset and the Measure tab labels them UNCALIBRATED along with the phone's model. No model has an entry yet. See Calibrate a phone model.

Privacy

The policy for store listings is PRIVACY.md. Keep it in step with this list.

  • Audio is processed on the device and never recorded or stored.
  • Adding a measurement to the map is always your choice, and it is shared with everyone.
  • Only the noise summary, the H3 cell, the time, and the phone model with its calibration are sent, plus a venue's OpenStreetMap id and the hour when you name one. Raw GPS coordinates never leave the phone.
  • Planning a walk happens on the phone. It downloads the walking map around your trip the way the map downloads its tiles; your start and end never leave the phone.
  • There are no accounts. Each install signs in as an anonymous user, used to recognize retried uploads and to limit each install to 120 readings an hour. Readings past the limit wait on the phone and upload later.
  • To list nearby venues, the phone sends our server the block it would share anyway. The server asks OpenStreetMap, so OpenStreetMap never sees the phone.
  • The map only ever shows per-block and per-venue averages and counts. Who measured, and when, stays on the server.

Run it

You need Node 24+ and Xcode for iOS, or Android Studio for Android. The app uses a native module, so it does not run in Expo Go.

The backend is Convex. npx convex dev logs you in, pushes convex/ to your dev deployment, and writes its URL to .env.local, which the app reads.

npm install
npx convex dev         # keep it running: it re-pushes convex/ on save
npx expo run:ios       # or: npx expo run:android

On a new deployment, set the Convex Auth signing keys (JWT_PRIVATE_KEY and JWKS) once, as described in Convex Auth's manual setup. To empty a dev map, set SOUNDMAP_ALLOW_RESET=true on that deployment and run npx convex run dev:resetMap.

To take one user's readings off the map, such as someone flooding it, copy the userId from one of their rows in the dashboard's measurements table and run npx convex run moderation:removeUser '{"userId": "<id>"}'. Add --prod for the production map. Their readings leave the block and venue averages too. A run removes 500 and schedules the next until none are left.

On the iOS Simulator the sound meter uses a synthetic café soundscape instead of the Mac microphone, so you can test the whole flow without a real device.

npm test               # unit tests (node --test)
npx tsc --noEmit       # typecheck
npx expo lint

Ship a build

Release builds run in the cloud on EAS. eas.json has two profiles. production makes store builds, for TestFlight and Google Play, and EAS raises the build number on each one. preview makes builds that testers install directly: an APK on Android and an ad hoc build on iOS.

The EAS project is @stivenkn/soundmap, and its id is in app.json. Sign in to that Expo account with npx eas-cli@latest login. The owner of the Convex and Apple Developer accounts does this once:

npx convex deploy         # pushes convex/ to the production deployment

Set the Convex Auth signing keys on the production deployment the same way as on dev. npx convex dashboard --prod opens it, and its settings page shows the deployment URL. The app needs that URL at build time, and .env.local never reaches EAS, so store it as an EAS environment variable for both profiles. Preview builds then use the production map too.

npx eas-cli@latest env:set --name EXPO_PUBLIC_CONVEX_URL --value <production URL> \
  --environment production --environment preview --visibility plaintext

Then build and send the build to TestFlight. The first build asks you to sign in to your Apple Developer account and sets up signing.

npx eas-cli@latest build --platform ios --profile production
npx eas-cli@latest submit --platform ios

For a preview build on an iPhone, register the phone first with npx eas-cli@latest device:create.

Google Play

The same production profile makes an Android App Bundle, and EAS raises its version code on each build. Once, create the app in Play Console with the package com.skylive.soundmap, then give EAS a Google service account key so it can upload for you (Expo's guide). Then:

npx eas-cli@latest build --platform android --profile production
npx eas-cli@latest submit --platform android

The upload goes to the internal testing track. Promote it to open testing in Play Console. Play asks for a privacy policy URL; use PRIVACY.md on GitHub.

Layout

  • convex/ is the backend: the schema, anonymous auth, measurements.ts, which adds a reading and serves the per-cell averages for the regions the map is showing, venues.ts, which serves venues' hourly averages, moderation.ts, which takes one user's readings off the map, and venueSearch.ts, which finds venues near a block and saves the result per block.
  • modules/sound-meter/ is the native module (Swift and Kotlin). It applies A-weighting and emits a level every 125 ms.
  • src/lib/ holds the pure logic: acoustics, per-model calibration, H3 cells and regions, the rules a measurement must pass, the loudness color scale, street noise and reading it from the tiles, the walking graph tiles, planning quiet walks and the route card's copy, venues, and the offline outbox. convex/ imports from it too.
  • src/hooks/ holds the measurement state machine, the "add to map" flow, the sync that signs in and uploads queued readings, and the walk plan that fetches a trip's walking tiles and measured blocks.
  • src/app/ holds the screens (Expo Router): the Map and Measure tabs. src/components/ holds larger pieces of those screens, such as the venue card, the route card and the route's map layers.
  • scripts/ holds the street noise and walking graph pipelines. Each writes one PMTiles archive, hosted on Cloudflare R2 rather than shipped in the app.
  • assets/brand/mark.svg is the logo. scripts/brand-icons.sh renders the app icon, the Android adaptive icon layers, the splash image and the favicon from it, and needs ImageMagick (brew install imagemagick).
  • patches/ holds a patch-package fix that lets h3-js run on Hermes (uber/h3-js#203).

Refresh street noise data

The map reads street noise from street-noise-<release>.pmtiles, a single file of vector tiles that MapLibre and the app read with HTTP range requests. To build it from a new NTNM release:

  1. In a browser, download the "Aviation, Rail, and Road Noise" zips for CONUS and Alaska, and "Hawaii Aviation and Road Noise" (Hawaii has no rail layer), from the NTNM page and unzip them. The server blocks curl.

  2. Install the tools: brew install gdal tippecanoe. GDAL reads the File Geodatabase rasters; tippecanoe writes the tiles.

  3. Run the pipeline with one --raster per raster. Its layer is the RASTER_DATASET that gdalinfo <gdb> prints, such as HI_road_noise. CONUS is one raster per state, so it takes 49 of them:

    npm run street-noise -- --release 2022 \
      --raster OpenFileGDB:<CONUS gdb>:<state layer>... --raster OpenFileGDB:<Alaska gdb>:<layer> --raster OpenFileGDB:<Hawaii gdb>:<layer>

    It reads the rasters in 1° windows in parallel, into .street-noise/blocks.tsv (one line per block). A window on a state line reads both states' rasters, so a block there averages pixels from each side. That takes hours for the whole country, so --blocks .street-noise/blocks.tsv rebuilds only the tiles from an earlier run. The tiles also carry the modeled area (the 50 states and DC, from Census cartographic boundaries), which tells a quiet block from one DOT doesn't model.

  4. Upload the archive to the Cloudflare R2 bucket with rclone, set up once as a remote named r2 with your R2 API keys (the dashboard can't upload files this large): rclone copyto .street-noise/street-noise-2022.pmtiles r2:soundmap-tiles/street-noise-2022.pmtiles. Then bump STREET_NOISE_RELEASE in src/lib/street-noise.ts; STREET_NOISE_TILES follows it. A new release gets a new file name, so phones never mix two releases.

To try tiles before uploading them, serve .street-noise/ with any server that supports range requests (npx http-server .street-noise -p 8791) and point a dev build at it in .env.local: EXPO_PUBLIC_STREET_NOISE_TILES=pmtiles://http://127.0.0.1:8791/street-noise-2022.pmtiles.

Refresh the walking graph

Quiet routes are planned on a walking graph cut from OpenStreetMap, walking-v<format>-<osm>-noise-<ntnm>.pmtiles. Each z14 tile holds the walkable ways in it, cut where they cross from one block into the next, with each block's modeled street noise baked in as the map reads it: a level, below the floor, or not covered. The phone reads the tiles around a trip with HTTP range requests. The name carries the tile format, the OSM snapshot and the NTNM release (walkTilesName in src/lib/walk-tiles.ts), so rebuild the graph when any of them changes.

  1. Download the OpenStreetMap extracts to cover from Geofabrik, such as california-latest.osm.pbf, or us-latest.osm.pbf for the whole country.

  2. Install the tools: brew install osmium-tool pmtiles coreutils. osmium reads the extracts, pmtiles writes the archive, and coreutils supplies the GNU sort and join the pipeline needs.

  3. Run the pipeline with the extract's snapshot month as the release and the blocks.tsv from the street noise build of the current STREET_NOISE_RELEASE:

    npm run walk-graph -- --release 2026-10 --osm california-latest.osm.pbf --blocks .street-noise/blocks.tsv

    It writes .walk-graph/walking-v2-2026-10-noise-2022.pmtiles; California takes 10 to 15 minutes. Repeat --osm to combine extracts; a way two of them share is kept once. Only the 50 states and DC that NTNM models are kept, out to state waters so piers stay whole (Census TIGER/Line boundaries, fetched once into .walk-graph/states-waters.geojson), so an extract that crosses a border, like California's into Mexico, stops at it. A block without DOT pixels is below the floor if its center lies inside the states' generalized shoreline, the street noise coverage layer (fetched once into .walk-graph/states-shoreline.geojson), and not covered past it, as on a pier. Where distinct OSM nodes on different levels share a point (a bridge over a street), all but one move a centimeter apart, so the graph doesn't join them. The build stops without writing the archive if an edge lies outside its block, fewer than 90 % of the nodes are in networks that cross a tile edge (nodes that stop joining leave almost none), or less than 5 % of the graph lies in modeled blocks, which means --blocks is for somewhere else.

  4. Upload the archive with rclone, as for street noise: rclone copyto .walk-graph/walking-v<format>-<osm>-noise-<ntnm>.pmtiles r2:soundmap-tiles/walking-v<format>-<osm>-noise-<ntnm>.pmtiles. Then bump WALK_GRAPH_RELEASE in src/lib/walk-tiles.ts; WALK_TILES follows it, the tile format and STREET_NOISE_RELEASE. Each gets a new file name, so phones never join tiles from two.

To try a graph before uploading it, serve .walk-graph/ with a server that supports range requests (npx http-server .walk-graph -p 8792) and set EXPO_PUBLIC_WALK_TILES=http://127.0.0.1:8792/walking-v2-2026-10-noise-2022.pmtiles in .env.local.

Calibrate a phone model

You need the phone, a speaker that plays steady pink noise, a quiet room, and a reference sound level meter (class 1 or 2, A-weighted, set to LAeq or slow).

  1. Open the Measure tab. While the phone is uncalibrated, the label shows its model, such as UNCALIBRATED · iPhone16,1. Use that model as the key in CALIBRATIONS.
  2. Play the pink noise at about 70 to 80 dBA. Hold the phone's microphone and the meter side by side, about a meter from the speaker.
  3. Run the app's 30 second measurement and the meter's LAeq over the same 30 seconds. Write down both LAeq values. The app shows LAeq rounded to a whole dB, so use that number. The result is good to about half a dB.
  4. Do this 3 times.
  5. Work out the offset: UNCALIBRATED_OFFSET_DB + (meter LAeq − app LAeq), averaged over the 3 runs. UNCALIBRATED_OFFSET_DB is in src/lib/calibration.ts.
  6. Add an entry to CALIBRATIONS with that offset and a new id, for example 'iPhone16,1': { id: 'iPhone16,1/2026-10', offsetDb: 117.5 }.

Roadmap

  • Calibrate the most common phone models.

License

MIT. See LICENSE.

About

Waze for noise: a crowdsourced sound map, starting in San Francisco

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages