> For the complete documentation index, see [llms.txt](https://docs.mapbox.com/mapbox-gl-js/ja/llms.txt)

# Animate 3D buildings based on ambient sounds

This example uses [runtime styling](https://docs.mapbox.com/help/glossary/runtime-styling/) with the [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) to create a map where the 3D buildings dynamically change height to the rhythm of your ambient environment, giving the appearance of dancing.

It uses [`getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) to access audio from the user's environment, then analyzes that sound for loudness and uses the loudness levels to adjust the [`fill-extrusion-height`](https://docs.mapbox.com/style-spec/reference/layers/?size=n_10_n#paint-fill-extrusion-fill-extrusion-height) property of the [`fill-extrusion` layer](https://docs.mapbox.com/style-spec/reference/layers/#fill-extrusion).

To optimize performance, it uses the [`==`](https://docs.mapbox.com/style-spec/reference/expressions/#==), [`>`](https://docs.mapbox.com/style-spec/reference/expressions/#%3E) and [`<=`](https://docs.mapbox.com/style-spec/reference/expressions/#%3C=) expression operators to filter buildings in the layer based on their height data.

> Example code:

**JavaScript**

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Animate 3D buildings based on ambient sounds</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v3.28.1/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.28.1/mapbox-gl.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>

<script>
    // Use a minimal variant of the Mapbox Dark style, with certain features removed.
    const map = new mapboxgl.Map({
        // TO MAKE THE MAP APPEAR YOU MUST
        // ADD YOUR ACCESS TOKEN FROM
        // https://account.mapbox.com
        accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN',
        // Choose from Mapbox's core styles, or make your own style with Mapbox Studio
        style: 'mapbox://styles/examples/cj68bstx01a3r2rndlud0pwpv',
        center: {
            lng: -74.0064,
            lat: 40.7081
        },
        zoom: 15,
        pitch: 55,
        container: 'map',
        antialias: true
    });

    map.addControl(new mapboxgl.FullscreenControl());

    map.on('load', () => {
        const bins = 16;
        const maxHeight = 200;
        const binWidth = maxHeight / bins;

        // Divide the buildings into 16 bins based on their true height, using a layer filter.
        for (let i = 0; i < bins; i++) {
            map.addLayer({
                'id': `3d-buildings-${i}`,
                'source': 'composite',
                'source-layer': 'building',
                'filter': [
                    'all',
                    ['==', 'extrude', 'true'],
                    ['>', 'height', i * binWidth],
                    ['<=', 'height', (i + 1) * binWidth]
                ],
                'type': 'fill-extrusion',
                'minzoom': 15,
                'paint': {
                    'fill-extrusion-color': '#aaa',
                    'fill-extrusion-height-transition': {
                        duration: 0,
                        delay: 0
                    },
                    'fill-extrusion-opacity': 0.6
                }
            });
        }

        // Older browsers might not implement mediaDevices at all, so we set an empty object first
        if (navigator.mediaDevices === undefined) {
            navigator.mediaDevices = {};
        }

        // Some browsers partially implement mediaDevices. We can't just assign an object
        // with getUserMedia as it would overwrite existing properties.
        // Here, we will just add the getUserMedia property if it's missing.
        if (navigator.mediaDevices.getUserMedia === undefined) {
            navigator.mediaDevices.getUserMedia = (constraints) => {
                // First get ahold of the legacy getUserMedia, if present
                const getUserMedia =
                    navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

                // Some browsers just don't implement it - return a rejected promise with an error
                // to keep a consistent interface
                if (!getUserMedia) {
                    return Promise.reject(
                        new Error(
                            'getUserMedia is not implemented in this browser'
                        )
                    );
                }

                // Otherwise, wrap the call to the old navigator.getUserMedia with a Promise
                return new Promise((resolve, reject) => {
                    getUserMedia.call(navigator, constraints, resolve, reject);
                });
            };
        }

        navigator.mediaDevices
            .getUserMedia({ audio: true })
            .then((stream) => {
                // Set up a Web Audio AudioContext and AnalyzerNode, configured to return the
                // same number of bins of audio frequency data.
                const audioCtx = new (window.AudioContext ||
                    window.webkitAudioContext)();

                const analyser = audioCtx.createAnalyser();
                analyser.minDecibels = -90;
                analyser.maxDecibels = -10;
                analyser.smoothingTimeConstant = 0.85;

                const source = audioCtx.createMediaStreamSource(stream);
                source.connect(analyser);

                analyser.fftSize = bins * 2;

                const dataArray = new Uint8Array(bins);

                function draw(now) {
                    analyser.getByteFrequencyData(dataArray);

                    // Use that data to drive updates to the fill-extrusion-height property.
                    let avg = 0;
                    for (let i = 0; i < bins; i++) {
                        avg += dataArray[i];
                        map.setPaintProperty(
                            `3d-buildings-${i}`,
                            'fill-extrusion-height',
                            10 + 4 * i + dataArray[i]
                        );
                    }
                    avg /= bins;

                    // Animate the map bearing and light color over time, and make the light more
                    // intense when the audio is louder.
                    map.setBearing(now / 500);
                    const hue = (now / 100) % 360;
                    const saturation = Math.min(50 + avg / 4, 100);
                    map.setLight({
                        color: `hsl(${hue},${saturation}%,50%)`,
                        intensity: Math.min(1, (avg / 256) * 10)
                    });

                    requestAnimationFrame(draw);
                }

                requestAnimationFrame(draw);
            })
            .catch((err) => {
                console.log('The following gUM error occurred:', err);
            });
    });
</script>

</body>
</html>
```

**React**

```jsx
import React, { useEffect, useRef } from 'react';
import * as mapboxgl from 'mapbox-gl/esm';
import 'mapbox-gl/dist/mapbox-gl.css';

const MapboxExample = () => {
  const mapContainerRef = useRef();
  const mapRef = useRef();

  useEffect(() => {
    mapRef.current = new mapboxgl.Map({
      // TO MAKE THE MAP APPEAR YOU MUST
      // ADD YOUR ACCESS TOKEN FROM
      // https://account.mapbox.com
      accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN',
      style: 'mapbox://styles/examples/cj68bstx01a3r2rndlud0pwpv',
      center: {
        lng: -74.0064,
        lat: 40.7081
      },
      zoom: 15,
      pitch: 55,
      container: mapContainerRef.current,
      antialias: true
    });

    mapRef.current.addControl(new mapboxgl.FullscreenControl());

    mapRef.current.on('load', () => {
      const bins = 16;
      const maxHeight = 200;
      const binWidth = maxHeight / bins;

      // Divide the buildings into 16 bins based on their true height, using a layer filter.
      for (let i = 0; i < bins; i++) {
        mapRef.current.addLayer({
          id: `3d-buildings-${i}`,
          source: 'composite',
          'source-layer': 'building',
          filter: [
            'all',
            ['==', 'extrude', 'true'],
            ['>', 'height', i * binWidth],
            ['<=', 'height', (i + 1) * binWidth]
          ],
          type: 'fill-extrusion',
          minzoom: 15,
          paint: {
            'fill-extrusion-color': '#aaa',
            'fill-extrusion-height-transition': {
              duration: 0,
              delay: 0
            },
            'fill-extrusion-opacity': 0.6
          }
        });
      }

      // Older browsers might not implement mediaDevices at all, so we set an empty object first
      if (navigator.mediaDevices === undefined) {
        navigator.mediaDevices = {};
      }

      // Some browsers partially implement mediaDevices. We can't just assign an object
      // with getUserMedia as it would overwrite existing properties.
      // Here, we will just add the getUserMedia property if it's missing.
      if (navigator.mediaDevices.getUserMedia === undefined) {
        navigator.mediaDevices.getUserMedia = (constraints) => {
          // First get ahold of the legacy getUserMedia, if present
          const getUserMedia =
            navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

          // Some browsers just don't implement it - return a rejected promise with an error
          // to keep a consistent interface
          if (!getUserMedia) {
            return Promise.reject(
              new Error('getUserMedia is not implemented in this browser')
            );
          }

          // Otherwise, wrap the call to the old navigator.getUserMedia with a Promise
          return new Promise((resolve, reject) => {
            getUserMedia.call(navigator, constraints, resolve, reject);
          });
        };
      }

      navigator.mediaDevices
        .getUserMedia({ audio: true })
        .then((stream) => {
          // Set up a Web Audio AudioContext and AnalyzerNode, configured to return the
          // same number of bins of audio frequency data.
          const audioCtx = new (window.AudioContext ||
            window.webkitAudioContext)();

          const analyser = audioCtx.createAnalyser();
          analyser.minDecibels = -90;
          analyser.maxDecibels = -10;
          analyser.smoothingTimeConstant = 0.85;

          const source = audioCtx.createMediaStreamSource(stream);
          source.connect(analyser);

          analyser.fftSize = bins * 2;

          const dataArray = new Uint8Array(bins);

          function draw(now) {
            analyser.getByteFrequencyData(dataArray);

            // Use that data to drive updates to the fill-extrusion-height property.
            let avg = 0;
            for (let i = 0; i < bins; i++) {
              avg += dataArray[i];
              mapRef.current.setPaintProperty(
                `3d-buildings-${i}`,
                'fill-extrusion-height',
                10 + 4 * i + dataArray[i]
              );
            }
            avg /= bins;

            // Animate the map bearing and light color over time, and make the light more
            // intense when the audio is louder.
            mapRef.current.setBearing(now / 500);
            const hue = (now / 100) % 360;
            const saturation = Math.min(50 + avg / 4, 100);
            mapRef.current.setLight({
              color: `hsl(${hue},${saturation}%,50%)`,
              intensity: Math.min(1, (avg / 256) * 10)
            });

            requestAnimationFrame(draw);
          }

          requestAnimationFrame(draw);
        })
        .catch((err) => {
          console.log('The following gUM error occurred:', err);
        });
    });

    return () => {
      mapRef.current.remove();
    };
  }, []);

  return <div ref={mapContainerRef} id="map" style={{ height: '100%' }}></div>;
};

export default MapboxExample;
```