<PositionalAudio>
Creates a positional audio entity. This uses the Web Audio API.
You need to have an <AudioListener> component in your scene in order to use <Audio>and <PositionalAudio>components. The <AudioListener> component needs to be mounted before any <Audio> or <PositionalAudio> components:
<T.PerspectiveCamera makeDefault> <AudioListener /></T.PerspectiveCamera>
<PositionalAudio /><script lang="ts"> import { Canvas } from '@threlte/core' import Scene from './Scene.svelte'</script>
<div> <Canvas> <Scene /> </Canvas></div>
<style> div { height: 100%; }</style><script lang="ts"> import { T } from '@threlte/core' import { Edges, Text, useCursor } from '@threlte/extras' import { Spring } from 'svelte/motion' import { MathUtils } from 'three' import type { ButtonProps } from './types'
let { text, onClick, ...rest }: ButtonProps = $props()
const buttonOffsetY = new Spring(0)
let buttonColor = $state('#111111') let textColor = $state('#eedbcb')
const { onPointerEnter, onPointerLeave } = useCursor()</script>
<T.Group {...rest}> <T.Group position.y={0.05 - buttonOffsetY.current}> <T.Mesh onclick={onClick} onpointerenter={(e) => { e.stopPropagation() buttonColor = '#eedbcb' textColor = '#111111' onPointerEnter() }} onpointerleave={(e) => { e.stopPropagation() buttonColor = '#111111' textColor = '#eedbcb' buttonOffsetY.set(0) onPointerLeave() }} onpointerdown={(e) => { e.stopPropagation() buttonOffsetY.set(0.05) }} onpointerup={(e) => { e.stopPropagation() buttonOffsetY.set(0) }} > <T.BoxGeometry args={[1.2, 0.1, 0.8]} /> <T.MeshStandardMaterial color={buttonColor} />
<Edges color="black" raycast={() => { return false }} /> </T.Mesh> <Text renderOrder={-100} ignorePointer color={textColor} {text} rotation.x={MathUtils.DEG2RAD * -90} position.y={0.055} fontSize={0.35} anchorX="50%" anchorY="50%" /> </T.Group></T.Group><script lang="ts"> import { T, useTask } from '@threlte/core' import { Edges, useGltf } from '@threlte/extras' import type { Mesh } from 'three' import type { DiscProps } from './types'
let { discSpeed = 0, ...rest }: DiscProps = $props()
let discRotation = $state(0) const { start, stop, started } = useTask( (delta) => { discRotation += delta * discSpeed }, { autoStart: false } )
$effect(() => { if (discSpeed <= 0 && $started) stop() else if (discSpeed > 0 && !$started) start() })
const gltf = useGltf<{ nodes: { Logo: Mesh } materials: {} }>('/models/turntable/disc-logo.glb')
const logoGeometry = $derived($gltf?.nodes.Logo.geometry)</script>
<T.Group {...rest}> <T.Group rotation.y={-discRotation}> <!-- DISH (?) --> <T.Mesh receiveShadow castShadow position.y={0.1} > <T.CylinderGeometry args={[1.85, 2, 0.2, 64]} /> <T.MeshStandardMaterial color="#111111" /> <Edges color="black" thresholdAngle={20} /> </T.Mesh>
<!-- ACTUAL DISC --> <T.Mesh receiveShadow castShadow position.y={0.2 + 0.05} > <T.CylinderGeometry args={[1.75, 1.75, 0.05, 64]} /> <T.MeshStandardMaterial color="#111111" /> <Edges thresholdAngle={50} scale={1} color="black" /> </T.Mesh>
<!-- ROUND LABEL --> <T.Mesh receiveShadow castShadow position.y={0.2 + 0.05 + 0.005} > <T.CylinderGeometry args={[0.8, 0.8, 0.05, 64]} /> <T.MeshStandardMaterial color="#eedbcb" /> <Edges thresholdAngle={50} scale={1} color="black" /> </T.Mesh>
<!-- LOGO --> {#if logoGeometry} <T.Mesh geometry={logoGeometry} position.y={0.2 + 0.05 + 0.025 + 0.01} > <T.MeshBasicMaterial color="#ff3e00" toneMapped={false} /> </T.Mesh> {/if} </T.Group></T.Group><script lang="ts"> import { T, useThrelte } from '@threlte/core' import { AudioListener, Environment, interactivity, OrbitControls } from '@threlte/extras' import { Spring } from 'svelte/motion' import { MathUtils } from 'three' import Speaker from './Speaker.svelte' import Turntable from './Turntable.svelte'
let volume = $state(0) let isPlaying = $state(false)
const smoothVolume = new Spring(0) $effect(() => { smoothVolume.set(volume) })
const { size } = useThrelte()
let zoom = $derived($size.width / 18)
interactivity({ filter: (hits) => { // only return first hit, we don't care // about propagation in this example return hits.slice(0, 1) } })</script>
<Environment url="/textures/equirectangular/hdr/shanghai_riverside_1k.hdr" />
<T.OrthographicCamera {zoom} makeDefault oncreate={(ref) => { ref.position.set(6, 9, 9) ref.lookAt(0, 1.5, 0) }}> <OrbitControls autoRotate={isPlaying} autoRotateSpeed={0.5} enableDamping maxPolarAngle={MathUtils.DEG2RAD * 80} target.y={1.5} /> <AudioListener /></T.OrthographicCamera>
<!-- FLOOR --><T.Mesh receiveShadow rotation.x={MathUtils.DEG2RAD * -90}> <T.CircleGeometry args={[10, 64]} /> <T.MeshStandardMaterial color="#333333" /></T.Mesh>
<Turntable bind:isPlaying bind:volume/>
<Speaker position.x={6} rotation.y={MathUtils.DEG2RAD * -7} {volume}/><Speaker position.x={-6} rotation.y={MathUtils.DEG2RAD * 7} {volume}/>
<T.DirectionalLight castShadow shadow.camera.left={-10} shadow.camera.bottom={-10} shadow.camera.right={10} shadow.camera.top={10} position={[10, 20, 8]} intensity={0.3}/><script lang="ts"> import { MathUtils } from 'three' import { T } from '@threlte/core' import { Edges } from '@threlte/extras' import { cubicIn, cubicOut } from 'svelte/easing' import { Tween } from 'svelte/motion' import type { SpeakerProps } from './types'
let { volume = 0, ...rest }: SpeakerProps = $props()
let jumpOffsetY = new Tween(0) let jumpRotationX = new Tween(0) let jumpRotationZ = new Tween(0) let isJumping = $state(false)
const randomSign = () => Math.round(Math.random()) * 2 - 1
const jump = () => { isJumping = true const upDuration = 10 + Math.random() * 50
jumpOffsetY.set(0.2, { duration: upDuration, easing: cubicOut }) jumpRotationX.set(Math.random() * 4 * randomSign(), { duration: upDuration, easing: cubicOut }) jumpRotationZ.set(Math.random() * 4 * randomSign(), { duration: upDuration, easing: cubicOut })
setTimeout(() => { const downDuration = 40 + Math.random() * 70
jumpOffsetY.set(0, { duration: downDuration, easing: cubicIn }) jumpRotationX.set(0, { duration: downDuration, easing: cubicIn }) jumpRotationZ.set(0, { duration: downDuration, easing: cubicIn })
setTimeout(() => { isJumping = false }, downDuration * 1.5) }, upDuration) }
$effect(() => { if (volume > 0.25 && !isJumping) jump() })</script>
<T.Group {...rest}> <T.Group position.y={jumpOffsetY.current} rotation.z={MathUtils.DEG2RAD * jumpRotationZ.current} rotation.x={MathUtils.DEG2RAD * jumpRotationX.current} > <!-- CASE --> <T.Mesh castShadow receiveShadow position.y={2.5} > <T.BoxGeometry args={[3, 5, 3]} /> <T.MeshStandardMaterial color="#eedbcb" /> <Edges color={'black'} scale={1.001} /> </T.Mesh>
<!-- CONE --> <T.Mesh position.z={1.1} position.y={3.5} scale={1 + volume} rotation.x={MathUtils.DEG2RAD * -90} > <T.ConeGeometry args={[1, 1, 64]} /> <T.MeshStandardMaterial flatShading color="#111111" /> <Edges color="black" scale={1.001} thresholdAngle={20} /> </T.Mesh> </T.Group></T.Group><script lang="ts"> import { T, useTask } from '@threlte/core' import { Edges, PositionalAudio, useAudioListener, useCursor, useGltf } from '@threlte/extras' import { Spring, Tween } from 'svelte/motion' import { CylinderGeometry, DoubleSide, Mesh, MeshStandardMaterial, PositionalAudio as ThreePositionalAudio, MathUtils } from 'three' import Button from './Button.svelte' import Disc from './Disc.svelte' import type { TurntableProps } from './types'
let { isPlaying = $bindable(false), volume = $bindable(0), ...rest }: TurntableProps = $props()
let discSpeed = new Tween(0, { duration: 1e3 })
let armPos = new Spring(0)
let started = $state(false)
export const toggle = async () => { if (!started) { await context.resume() started = true } if (isPlaying) { discSpeed.set(0) armPos.set(0) isPlaying = false } else { discSpeed.set(1) armPos.set(1) isPlaying = true } }
let audio = $state.raw<ThreePositionalAudio>()
const { context } = useAudioListener() const analyser = context.createAnalyser() $effect(() => { if (audio) audio.getOutput().connect(analyser) }) const pcmData = new Float32Array(analyser.fftSize) useTask(() => { if (!audio) return analyser.getFloatTimeDomainData(pcmData) let sumSquares = 0.0 for (const amplitude of pcmData) { sumSquares += amplitude * amplitude } volume = Math.sqrt(sumSquares / pcmData.length) })
let sideA = '/audio/side_a.mp3' let sideB = '/audio/side_b.mp3' let source = $state(sideA) const changeSide = () => { source = source === sideA ? sideB : sideA }
let coverOpen = $state(false) const coverAngle = new Spring(0) $effect(() => { if (coverOpen) coverAngle.set(80) else coverAngle.set(0) })
const { onPointerEnter, onPointerLeave } = useCursor()
const gltf = useGltf<{ nodes: { Cover: Mesh } materials: {} }>('/models/turntable/cover.glb')
const coverGeometry = $derived($gltf?.nodes.Cover.geometry)</script>
<T.Group {...rest}> <!-- DISC --> <Disc position.x={0.5} position.y={1.01} discSpeed={discSpeed.current} />
<!-- CASE --> <T.Mesh receiveShadow castShadow position.y={0.5} > <T.BoxGeometry args={[6, 1, 4.4]} /> <T.MeshStandardMaterial color="#eedbcb" /> <Edges scale={1.001} color="black" /> </T.Mesh>
<!-- COVER --> <T.Group position.y={1} position.z={-2.2} rotation.x={-coverAngle.current * MathUtils.DEG2RAD} > {#if coverGeometry} <T.Mesh geometry={coverGeometry} scale={[3, 0.5, 2.2]} position.y={0.5} position.z={2.2} onclick={() => (coverOpen = !coverOpen)} onpointerenter={onPointerEnter} onpointerleave={onPointerLeave} > <T.MeshStandardMaterial color="#ffffff" roughness={0.08} metalness={0.8} envMapIntensity={1} side={DoubleSide} transparent opacity={0.65} /> <Edges color="white" /> </T.Mesh> {/if} </T.Group>
<!-- SIDE BUTTON --> <Button position={[-2.3, 1.01, 0.8]} onClick={changeSide} text={source === sideA ? 'SIDE B' : 'SIDE A'} />
<!-- PLAY/PAUSE BUTTON --> <Button position={[-2.3, 1.01, 1.7]} onClick={toggle} text={isPlaying ? 'PAUSE' : 'PLAY'} />
<!-- ARM --> <T.Group position={[2.5, 1.55, -1.8]} rotation.z={MathUtils.DEG2RAD * 90} rotation.y={MathUtils.DEG2RAD * 90 - armPos.current * 0.3} > <T.Mesh castShadow material={new MeshStandardMaterial({ color: 0xffffff })} geometry={new CylinderGeometry(0.1, 0.1, 3, 12)} position.y={1.5} > <T.CylinderGeometry args={[0.1, 0.1, 3, 12]} /> <T.MeshStandardMaterial color="#ffffff" /> <Edges color="black" thresholdAngle={80} /> </T.Mesh> </T.Group>
{#if started} <PositionalAudio autoplay bind:ref={audio} refDistance={15} loop playbackRate={discSpeed.current} src={source} directionalCone={{ coneInnerAngle: 90, coneOuterAngle: 220, coneOuterGain: 0.3 }} /> {/if}</T.Group>import type { Props } from '@threlte/core'import type { Group } from 'three'
export type TurntableProps = Props<Group> & { isPlaying?: boolean volume?: number}
export type SpeakerProps = Props<Group> & { volume?: number}
export type DiscProps = Props<Group> & { discSpeed?: number}
export type ButtonProps = Props<Group> & { text: string onClick: () => void}Example
Section titled “Example”<script> import { T, Canvas } from '@threlte/core' import { AudioListener, PositionalAudio } from '@threlte/extras' import Car from './Car.svelte'</script>
<Canvas> <T.PerspectiveCamera makeDefault position={[3, 3, 3]} lookAt={[0, 0, 0]} > <AudioListener /> </T.PerspectiveCamera>
<Car> <PositionalAudio autostart loop refDistance={10} volume={0.2} src={'/audio/car-noise.mp3'} /> </Car></Canvas>Component Signature
Section titled “Component Signature”
<PositionalAudio> extends
<
T
.
PositionalAudio
>
and supports all its props, snippets, bindings and events.