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

hiddentao / clockwork-engine / 19607899653

23 Nov 2025 07:35AM UTC coverage: 80.348% (-18.3%) from 98.679%
19607899653

push

github

web-flow
feat: refactor engine to be platform-independent (#3)

1300 of 1986 new or added lines in 25 files covered. (65.46%)

9 existing lines in 1 file now uncovered.

2911 of 3623 relevant lines covered (80.35%)

39.37 hits per line

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

94.21
/src/platform/web/WebAudioLayer.ts
1
/**
2
 * Web Audio Layer
3
 *
4
 * Web Audio API-based audio implementation.
5
 */
6

7
import { AudioContextState } from "../AudioLayer"
99✔
8
import type { AudioBuffer, AudioLayer } from "../AudioLayer"
9

10
export class WebAudioLayer implements AudioLayer {
43✔
11
  private context: AudioContext | null = null
34✔
12
  private buffers = new Map<string, AudioBuffer>()
40✔
13
  private activeSources = new Map<string, AudioBufferSourceNode[]>()
52✔
14
  private isClosed = false
42✔
15

16
  private async resumeWithTimeout(maxWaitMs = 2000): Promise<void> {
65✔
17
    if (!this.context) {
44✔
18
      return
11✔
19
    }
9✔
20

21
    await this.context.resume()
64✔
22

23
    const startTime = Date.now()
66✔
24
    while (
14✔
25
      this.context.state === AudioContextState.SUSPENDED &&
108✔
26
      Date.now() - startTime < maxWaitMs
71✔
27
    ) {
7✔
28
      await new Promise((resolve) => setTimeout(resolve, 10))
60✔
29
    }
15✔
30
  }
31

32
  async initialize(): Promise<void> {
26✔
33
    if (this.context) {
42✔
34
      return
11✔
35
    }
9✔
36
    this.context = new AudioContext()
72✔
37
    await this.resumeWithTimeout()
71✔
38
  }
39

40
  destroy(): void {
23✔
41
    if (!this.context) {
51✔
42
      this.isClosed = true
54✔
43
      return
17✔
44
    }
9✔
45

46
    this.stopAll()
38✔
47
    this.context.close()
50✔
48
    this.context = null
48✔
49
    this.buffers.clear()
50✔
50
    this.activeSources.clear()
62✔
51
    this.isClosed = true
55✔
52
  }
53

54
  async loadSound(id: string, data: string | ArrayBuffer): Promise<void> {
41✔
55
    if (!this.context) {
44✔
56
      return
11✔
57
    }
9✔
58

59
    if (typeof data === "string") {
66✔
60
      throw new Error("Loading from URL strings not yet implemented")
68✔
61
    }
9✔
62

63
    if (data.byteLength === 0) {
67✔
64
      const emptyBuffer = this.createBuffer(1, 1, 44100)
114✔
65
      this.buffers.set(id, emptyBuffer)
80✔
66
      return
17✔
67
    }
9✔
68

69
    try {
22✔
70
      const audioBuffer = await this.context.decodeAudioData(data)
134✔
71
      this.buffers.set(id, audioBuffer as AudioBuffer)
78✔
72
    } catch (error) {
21✔
73
      console.warn(`Failed to decode audio data for ${id}:`, error)
76✔
74
    }
75
  }
76

77
  createBuffer(
14✔
78
    channels: number,
20✔
79
    length: number,
16✔
80
    sampleRate: number,
24✔
81
  ): AudioBuffer {
10✔
82
    if (!this.context) {
44✔
83
      throw new Error("AudioContext not initialized")
52✔
84
    }
9✔
85
    return this.context.createBuffer(
66✔
86
      channels,
20✔
87
      length,
16✔
88
      sampleRate,
20✔
89
    ) as AudioBuffer
9✔
90
  }
91

92
  loadSoundFromBuffer(id: string, buffer: AudioBuffer): void {
55✔
93
    this.buffers.set(id, buffer)
66✔
94
  }
95

96
  playSound(id: string, volume = 1.0, loop = false): void {
81✔
97
    if (!this.context) {
51✔
98
      return
17✔
99
    }
9✔
100

101
    const buffer = this.buffers.get(id)
80✔
102
    if (!buffer) {
39✔
103
      return
17✔
104
    }
9✔
105

106
    const source = this.context.createBufferSource()
106✔
107
    source.buffer = buffer as any
54✔
108
    source.loop = loop
46✔
109

110
    const gainNode = this.context.createGain()
94✔
111
    gainNode.gain.value = volume
66✔
112

113
    source.connect(gainNode)
58✔
114
    gainNode.connect(this.context.destination)
94✔
115

116
    source.start()
38✔
117

118
    if (!this.activeSources.has(id)) {
79✔
119
      this.activeSources.set(id, [])
70✔
120
    }
9✔
121
    this.activeSources.get(id)!.push(source)
88✔
122

NEW
123
    source.onended = () => {
×
NEW
124
      const sources = this.activeSources.get(id)
×
NEW
125
      if (sources) {
×
NEW
126
        const index = sources.indexOf(source)
×
NEW
127
        if (index !== -1) {
×
NEW
128
          sources.splice(index, 1)
×
NEW
129
        }
×
130
      }
16✔
131
    }
132
  }
133

134
  stopSound(id: string): void {
29✔
135
    const sources = this.activeSources.get(id)
94✔
136
    if (!sources) {
41✔
137
      return
17✔
138
    }
9✔
139

140
    for (const source of sources) {
73✔
141
      try {
26✔
142
        source.stop()
42✔
143
      } catch {
16✔
144
        // Ignore errors from already stopped sources
145
      }
146
    }
9✔
147

148
    this.activeSources.delete(id)
68✔
149
  }
150

151
  stopAll(): void {
23✔
152
    for (const id of this.activeSources.keys()) {
101✔
153
      this.stopSound(id)
46✔
154
    }
14✔
155
  }
156

157
  async resumeContext(): Promise<void> {
29✔
158
    await this.resumeWithTimeout()
71✔
159
  }
160

161
  getState(): AudioContextState {
24✔
162
    if (this.isClosed) {
51✔
163
      return AudioContextState.CLOSED
67✔
164
    }
9✔
165
    if (!this.context) {
51✔
166
      return AudioContextState.SUSPENDED
73✔
167
    }
9✔
168
    return this.context.state as AudioContextState
56✔
169
  }
170
}
2✔
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