Skip to content

Commit 558451f

Browse files
authored
fix: audio source retrigger deduplication (#1382)
1 parent 8c5b5ad commit 558451f

3 files changed

Lines changed: 64 additions & 11 deletions

File tree

packages/@dcl/ecs/src/components/extended/AudioSource.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ export interface AudioSourceComponentDefinitionExtended
1111
/**
1212
* @public
1313
*
14-
* Set playing=true the sound `$name`
15-
* @param entity - entity with AudioSource component
14+
* Play the sound `src` on the given entity. Creates the AudioSource component
15+
* if it does not yet exist. Always emits a CRDT PUT, so repeated calls with
16+
* identical parameters reliably retrigger playback.
17+
* @param entity - target entity (AudioSource will be created if missing)
1618
* @param src - the path to the sound to play
1719
* @param resetCursor - the sound starts at 0 or continues from the current cursor position
18-
* @returns true in successful playing, false if it doesn't find the AudioSource component
20+
* @returns always true; retained for backwards compatibility
1921
*/
2022
playSound(entity: Entity, src: string, resetCursor?: boolean): boolean
2123

@@ -38,13 +40,14 @@ export function defineAudioSourceComponent(
3840
return {
3941
...theComponent,
4042
playSound(entity: Entity, src: string, resetCursor: boolean = true): boolean {
41-
// Get the mutable to modify
42-
const audioSource = theComponent.getMutableOrNull(entity)
43-
if (!audioSource) return false
43+
const existing = theComponent.getOrNull(entity)
4444

45-
audioSource.audioClipUrl = src
46-
audioSource.playing = true
47-
audioSource.currentTime = resetCursor ? 0 : audioSource.currentTime
45+
theComponent.createOrReplace(entity, {
46+
...existing,
47+
audioClipUrl: src,
48+
playing: true,
49+
currentTime: resetCursor ? 0 : existing?.currentTime ?? 0
50+
})
4851

4952
return true
5053
},

test/ecs/components/AudioSource.spec.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Engine, components } from '../../../packages/@dcl/ecs/src'
2+
import { CrdtMessageType } from '../../../packages/@dcl/ecs/src/serialization/crdt/types'
23
import { testComponentSerialization } from './assertion'
34

45
describe('Generated AudioSource ProtoBuf', () => {
@@ -38,8 +39,9 @@ describe('Generated AudioSource ProtoBuf', () => {
3839
playing: true
3940
})
4041

41-
expect(AudioSource.playSound(entityWithoutAudioSource, 'some-src')).toBe(false)
42+
// stopSound on an entity without AudioSource is a no-op.
4243
expect(AudioSource.stopSound(entityWithoutAudioSource)).toBe(false)
44+
expect(AudioSource.getOrNull(entityWithoutAudioSource)).toBeNull()
4345

4446
// play sound with new "src" & reset cursor
4547
expect(AudioSource.playSound(entity, 'other-src', true)).toBe(true)
@@ -60,4 +62,52 @@ describe('Generated AudioSource ProtoBuf', () => {
6062
expect(AudioSource.stopSound(entity, true)).toBe(true)
6163
expect(AudioSource.getOrNull(entity)).toStrictEqual({ audioClipUrl: 'other-src', playing: false, currentTime: 0 })
6264
})
65+
66+
it('should create the AudioSource component on entities that do not yet have one when playSound is called', () => {
67+
const newEngine = Engine()
68+
const AudioSource = components.AudioSource(newEngine)
69+
const entity = newEngine.addEntity()
70+
71+
// Entity has no AudioSource component yet.
72+
expect(AudioSource.getOrNull(entity)).toBeNull()
73+
74+
// playSound must create it and start playback.
75+
expect(AudioSource.playSound(entity, 'fresh.mp3')).toBe(true)
76+
expect(AudioSource.getOrNull(entity)).toStrictEqual({
77+
audioClipUrl: 'fresh.mp3',
78+
playing: true,
79+
currentTime: 0
80+
})
81+
82+
// And it must emit a CRDT PUT so the renderer hears about it.
83+
const messages = Array.from(AudioSource.getCrdtUpdates())
84+
expect(messages).toHaveLength(1)
85+
expect(messages[0].type).toBe(CrdtMessageType.PUT_COMPONENT)
86+
})
87+
88+
it('should emit a CRDT PUT on every playSound call, even with identical parameters (retrigger)', () => {
89+
const newEngine = Engine()
90+
const AudioSource = components.AudioSource(newEngine)
91+
const entity = newEngine.addEntity()
92+
93+
AudioSource.create(entity, { audioClipUrl: 'a.mp3', playing: false })
94+
95+
// Flush initial create
96+
const createMessages = Array.from(AudioSource.getCrdtUpdates())
97+
expect(createMessages).toHaveLength(1)
98+
expect(createMessages[0].type).toBe(CrdtMessageType.PUT_COMPONENT)
99+
100+
// First playSound call
101+
expect(AudioSource.playSound(entity, 'a.mp3')).toBe(true)
102+
const firstPlayMessages = Array.from(AudioSource.getCrdtUpdates())
103+
expect(firstPlayMessages).toHaveLength(1)
104+
expect(firstPlayMessages[0].type).toBe(CrdtMessageType.PUT_COMPONENT)
105+
106+
// Second playSound call with identical parameters — must still emit a PUT
107+
expect(AudioSource.playSound(entity, 'a.mp3')).toBe(true)
108+
const secondPlayMessages = Array.from(AudioSource.getCrdtUpdates())
109+
expect(secondPlayMessages).toHaveLength(1)
110+
expect(secondPlayMessages[0].type).toBe(CrdtMessageType.PUT_COMPONENT)
111+
})
112+
63113
})

test/snapshots/production-bundles/with-main-function.ts.crdt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,4 @@ CALL onUpdate(0.1)
3838
OPCODES ~= 5k
3939
MALLOC_COUNT = 13
4040
ALIVE_OBJS_DELTA ~= 0.00k
41-
MEMORY_USAGE_COUNT ~= 2964.64k bytes
41+
MEMORY_USAGE_COUNT ~= 2964.74k bytes

0 commit comments

Comments
 (0)