• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excaliburjs / Excalibur / 14804036802

02 May 2025 09:58PM UTC coverage: 5.927% (-83.4%) from 89.28%
14804036802

Pull #3404

github

web-flow
Merge 5c103d7f8 into 0f2ccaeb2
Pull Request #3404: feat: added Graph module to Math

234 of 8383 branches covered (2.79%)

229 of 246 new or added lines in 1 file covered. (93.09%)

13145 existing lines in 208 files now uncovered.

934 of 15759 relevant lines covered (5.93%)

4.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/engine/Resources/Sound/WebAudioInstance.ts
1
import { StateMachine } from '../../Util/StateMachine';
2
import { Audio } from '../../Interfaces/Audio';
3
import { clamp } from '../../Math/util';
4
import { AudioContextFactory } from './AudioContext';
5
import { Future } from '../../Util/Future';
6

7
interface SoundState {
8
  startedAt: number;
9
  pausedAt: number;
10
}
11

12
/**
13
 * Internal class representing a Web Audio AudioBufferSourceNode instance
14
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API
15
 */
16
export class WebAudioInstance implements Audio {
17
  private _instance!: AudioBufferSourceNode;
UNCOV
18
  private _audioContext: AudioContext = AudioContextFactory.create();
×
UNCOV
19
  private _volumeNode = this._audioContext.createGain();
×
20

UNCOV
21
  private _playingFuture = new Future<boolean>();
×
UNCOV
22
  private _stateMachine = StateMachine.create(
×
23
    {
24
      start: 'STOPPED',
25
      states: {
26
        PLAYING: {
27
          onEnter: ({ data }) => {
28
            // Buffer nodes are single use
UNCOV
29
            this._createNewBufferSource();
×
UNCOV
30
            this._handleEnd();
×
UNCOV
31
            if (this.loop) {
×
32
              // when looping don't set a duration
UNCOV
33
              this._instance.start(0, data.pausedAt * this._playbackRate);
×
34
            } else {
UNCOV
35
              this._instance.start(0, data.pausedAt * this._playbackRate, this.duration);
×
36
            }
UNCOV
37
            data.startedAt = this._audioContext.currentTime - data.pausedAt;
×
UNCOV
38
            data.pausedAt = 0;
×
39
          },
UNCOV
40
          onState: () => this._playStarted(),
×
41
          onExit: ({ to }) => {
42
            // If you've exited early only resolve if explicitly STOPPED
UNCOV
43
            if (to === 'STOPPED') {
×
UNCOV
44
              this._playingFuture.resolve(true);
×
45
            }
46
            // Whenever you're not playing... you stop!
UNCOV
47
            this._instance.onended = null; // disconnect the wired on-end handler
×
UNCOV
48
            this._instance.disconnect();
×
UNCOV
49
            this._instance.stop(0);
×
UNCOV
50
            this._instance = null as any;
×
51
          },
52
          transitions: ['STOPPED', 'PAUSED', 'SEEK']
53
        },
54
        SEEK: {
55
          onEnter: ({ eventData: position, data }: { eventData?: number; data: SoundState }) => {
UNCOV
56
            data.pausedAt = (position ?? 0) / this._playbackRate;
×
UNCOV
57
            data.startedAt = 0;
×
58
          },
59
          transitions: ['*']
60
        },
61
        STOPPED: {
62
          onEnter: ({ data }) => {
UNCOV
63
            data.pausedAt = 0;
×
UNCOV
64
            data.startedAt = 0;
×
UNCOV
65
            this._playingFuture.resolve(true);
×
66
          },
67
          transitions: ['PLAYING', 'PAUSED', 'SEEK']
68
        },
69
        PAUSED: {
70
          onEnter: ({ data }) => {
71
            // Playback rate will be a scale factor of how fast/slow the audio is being played
72
            // default is 1.0
73
            // we need to invert it to get the time scale
UNCOV
74
            data.pausedAt = this._audioContext.currentTime - data.startedAt;
×
75
          },
76
          transitions: ['PLAYING', 'STOPPED', 'SEEK']
77
        }
78
      }
79
    },
80
    {
81
      startedAt: 0,
82
      pausedAt: 0
83
    } satisfies SoundState
84
  );
85

86
  private _createNewBufferSource() {
UNCOV
87
    this._instance = this._audioContext.createBufferSource();
×
UNCOV
88
    this._instance.buffer = this._src;
×
UNCOV
89
    this._instance.loop = this.loop;
×
UNCOV
90
    this._instance.playbackRate.value = this._playbackRate;
×
UNCOV
91
    this._instance.connect(this._volumeNode);
×
UNCOV
92
    this._volumeNode.connect(this._audioContext.destination);
×
93
  }
94

95
  private _handleEnd() {
UNCOV
96
    if (!this.loop) {
×
UNCOV
97
      this._instance.onended = () => {
×
UNCOV
98
        this._playingFuture.resolve(true);
×
99
      };
100
    }
101
  }
102

UNCOV
103
  private _volume = 1;
×
UNCOV
104
  private _loop = false;
×
105
  // eslint-disable-next-line @typescript-eslint/no-empty-function
UNCOV
106
  private _playStarted: () => any = () => {};
×
107
  public set loop(value: boolean) {
UNCOV
108
    this._loop = value;
×
109

UNCOV
110
    if (this._instance) {
×
UNCOV
111
      this._instance.loop = value;
×
UNCOV
112
      if (!this.loop) {
×
UNCOV
113
        this._instance.onended = () => {
×
114
          this._playingFuture.resolve(true);
×
115
        };
116
      }
117
    }
118
  }
119
  public get loop(): boolean {
UNCOV
120
    return this._loop;
×
121
  }
122

123
  public set volume(value: number) {
UNCOV
124
    value = clamp(value, 0, 1.0);
×
125

UNCOV
126
    this._volume = value;
×
127

UNCOV
128
    if (this._stateMachine.in('PLAYING') && this._volumeNode.gain.setTargetAtTime) {
×
129
      // https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/setTargetAtTime
130
      // After each .1 seconds timestep, the target value will ~63.2% closer to the target value.
131
      // This exponential ramp provides a more pleasant transition in gain
UNCOV
132
      this._volumeNode.gain.setTargetAtTime(value, this._audioContext.currentTime, 0.1);
×
133
    } else {
UNCOV
134
      this._volumeNode.gain.value = value;
×
135
    }
136
  }
137
  public get volume(): number {
UNCOV
138
    return this._volume;
×
139
  }
140

141
  private _duration: number | undefined;
142
  /**
143
   * Returns the set duration to play, otherwise returns the total duration if unset
144
   */
145
  public get duration() {
UNCOV
146
    return this._duration ?? this.getTotalPlaybackDuration();
×
147
  }
148

149
  /**
150
   * Set the duration that this audio should play.
151
   *
152
   * Note: if you seek to a specific point the duration will start from that point, for example
153
   *
154
   * If you have a 10 second clip, seek to 5 seconds, then set the duration to 2, it will play the clip from 5-7 seconds.
155
   */
156
  public set duration(duration: number) {
UNCOV
157
    this._duration = duration;
×
158
  }
159

UNCOV
160
  constructor(private _src: AudioBuffer) {
×
UNCOV
161
    this._createNewBufferSource();
×
162
  }
163

164
  public isPlaying() {
UNCOV
165
    return this._stateMachine.in('PLAYING');
×
166
  }
167

168
  public isPaused() {
UNCOV
169
    return this._stateMachine.in('PAUSED') || this._stateMachine.in('SEEK');
×
170
  }
171

172
  public isStopped() {
173
    return this._stateMachine.in('STOPPED');
×
174
  }
175

176
  // eslint-disable-next-line @typescript-eslint/no-empty-function
177
  public play(playStarted: () => any = () => {}) {
×
UNCOV
178
    this._playStarted = playStarted;
×
UNCOV
179
    this._stateMachine.go('PLAYING');
×
UNCOV
180
    return this._playingFuture.promise;
×
181
  }
182

183
  public pause() {
UNCOV
184
    this._stateMachine.go('PAUSED');
×
185
  }
186

187
  public stop() {
UNCOV
188
    this._stateMachine.go('STOPPED');
×
189
  }
190

191
  public seek(position: number) {
UNCOV
192
    this._stateMachine.go('PAUSED');
×
UNCOV
193
    this._stateMachine.go('SEEK', position);
×
194
  }
195

196
  public getTotalPlaybackDuration() {
UNCOV
197
    return this._src.duration;
×
198
  }
199

200
  public getPlaybackPosition() {
UNCOV
201
    const { pausedAt, startedAt } = this._stateMachine.data;
×
UNCOV
202
    if (pausedAt) {
×
UNCOV
203
      return pausedAt * this._playbackRate;
×
204
    }
UNCOV
205
    if (startedAt) {
×
UNCOV
206
      return (this._audioContext.currentTime - startedAt) * this._playbackRate;
×
207
    }
208
    return 0;
×
209
  }
210

UNCOV
211
  private _playbackRate = 1.0;
×
212
  public set playbackRate(playbackRate: number) {
UNCOV
213
    this._instance.playbackRate.value = this._playbackRate = playbackRate;
×
214
  }
215

216
  public get playbackRate() {
217
    return this._instance.playbackRate.value;
×
218
  }
219
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc