Skip to content

<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
}
Music: legrisch
<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>

<PositionalAudio> extends < T . PositionalAudio > and supports all its props, snippets, bindings and events.

Props

name
type
required
default
description

src
string | AudioBuffer | HTMLMediaElement | AudioBufferSourceNode | MediaStream
yes

autoplay
boolean
no

detune
number
no

directionalCone
{ coneInnerAngle: number, coneOuterAngle: number, coneOuterGain: number }
no

distanceModel
string
no

id
string
no
default
The id of the AudioListener this Audio will be attached to.

loop
boolean
no

maxDistance
number
no

playbackRate
number
no

refDistance
number
no

rolloffFactor
number
no

volume
number
no

Events

name
payload
description

load
AudioBuffer
Fired when the audio has loaded.

progress
ProgressEvent<EventTarget>
Fired when the audio is loading.

error
ErrorEvent
Fired when the audio fails to load.

Exports

name
type

play
(delay?: number) => Promise<THREE.Audio>

pause
() => THREE.Audio

stop
() => THREE.Audio