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

melchor629 / node-flac-bindings / 13604713414

01 Mar 2025 12:22PM UTC coverage: 91.073% (+9.3%) from 81.781%
13604713414

push

github

melchor629
fix: native code coverage

159 of 202 branches covered (78.71%)

Branch coverage included in aggregate %.

4 of 5 new or added lines in 2 files covered. (80.0%)

72 existing lines in 2 files now uncovered.

5003 of 5466 relevant lines covered (91.53%)

37359.39 hits per line

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

74.27
/lib/decoder/stream-decoder.js
1
import stream from 'node:stream'
1✔
2
import debug from 'debug'
1✔
3
import * as flac from '../api.js'
1✔
4

5
class StreamDecoder extends stream.Transform {
1✔
6
  constructor(options = {}) {
1✔
7
    super({
12✔
8
      ...options,
12✔
9
      decodeStrings: false,
12✔
10
      defaultEncoding: undefined,
12✔
11
      encoding: undefined,
12✔
12
    })
12✔
13
    this._debug = debug('flac:decoder:stream')
12✔
14
    this._builder = new flac.DecoderBuilder()
12✔
15
    this._dec = null
12✔
16
    this._oggStream = options.isOggStream || false
12✔
17
    this._outputAs32 = options.outputAs32 || false
12✔
18
    this._chunks = []
12✔
19
    this._processedSamples = 0
12✔
20

21
    if (this._oggStream && !flac.format.API_SUPPORTS_OGG_FLAC) {
12!
22
      throw new Error('Ogg FLAC is unsupported')
×
23
    }
×
24

25
    if (options.metadata === true) {
12✔
26
      this._debug('Setting decoder to emit all metadata blocks')
1✔
27
      this._builder.setMetadataRespondAll()
1✔
28
    } else if (Array.isArray(options.metadata)) {
12✔
29
      this._debug(`Setting decoder to emit '${options.metadata.join(', ')}' metadata blocks`)
1✔
30
      for (const type of options.metadata) {
1✔
31
        this._builder.setMetadataRespond(type)
1✔
32
      }
1✔
33
    }
1✔
34

35
    if (options.isChainedStream) {
12✔
36
      this._builder.setDecodeChainedStream(true)
1✔
37
    }
1✔
38
  }
12✔
39

40
  get processedSamples() {
1✔
41
    return this._processedSamples
8✔
42
  }
8✔
43

44
  getTotalSamples() {
1✔
45
    if (this._totalSamples === undefined) {
2!
UNCOV
46
      this._totalSamples = this._dec.getTotalSamples()
×
UNCOV
47
    }
×
48
    return this._totalSamples
2✔
49
  }
2✔
50

51
  getChannels() {
1✔
52
    if (this._channels === undefined) {
1!
UNCOV
53
      this._channels = this._dec.getChannels()
×
UNCOV
54
    }
×
55
    return this._channels
1✔
56
  }
1✔
57

58
  getChannelAssignment() {
1✔
59
    if (this._channelAssignment === undefined) {
1✔
60
      this._channelAssignment = this._dec.getChannelAssignment()
1✔
61
    }
1✔
62
    return this._channelAssignment
1✔
63
  }
1✔
64

65
  getBitsPerSample() {
1✔
66
    if (this._bitsPerSample === undefined) {
329!
UNCOV
67
      this._bitsPerSample = this._dec.getBitsPerSample()
×
UNCOV
68
    }
×
69
    return this._bitsPerSample
329✔
70
  }
329✔
71

72
  getOutputBitsPerSample() {
1✔
73
    if (this._outputAs32) {
533✔
74
      return 32
205✔
75
    }
205✔
76

77
    return this.getBitsPerSample()
328✔
78
  }
533✔
79

80
  getSampleRate() {
1✔
81
    if (this._sampleRate === undefined) {
3!
UNCOV
82
      this._sampleRate = this._dec.getSampleRate()
×
UNCOV
83
    }
×
84
    return this._sampleRate
3✔
85
  }
3✔
86

87
  getProgress() {
1✔
88
    if (this._dec || (this._totalSamples && this._sampleRate)) {
1!
89
      const position = this._processedSamples
1✔
90
      const totalSamples = this.getTotalSamples()
1✔
91
      const percentage = totalSamples ? position / totalSamples : NaN
1!
92
      const totalSeconds = totalSamples ? totalSamples / this.getSampleRate() : NaN
1!
93
      const currentSeconds = position / this.getSampleRate()
1✔
94
      return {
1✔
95
        position,
1✔
96
        totalSamples,
1✔
97
        percentage,
1✔
98
        totalSeconds,
1✔
99
        currentSeconds,
1✔
100
      }
1✔
101
    }
1!
102

UNCOV
103
    return undefined
×
104
  }
1✔
105

106
  async _transform(chunk, _, callback) {
1✔
107
    try {
142✔
108
      if (!this._dec) {
142✔
109
        this._debug('Initializing decoder')
11✔
110
        try {
11✔
111
          if (this._oggStream) {
11✔
112
            this._debug('Initializing for Ogg/FLAC')
3✔
113
            this._dec = await this._builder.buildWithOggStreamAsync(
3✔
114
              this._readCbk.bind(this),
3✔
115
              null,
3✔
116
              null,
3✔
117
              null,
3✔
118
              null,
3✔
119
              this._writeCbk.bind(this),
3✔
120
              this._metadataCbk.bind(this),
3✔
121
              this._errorCbk.bind(this),
3✔
122
            )
3✔
123
          } else {
11✔
124
            this._debug('Initializing for FLAC')
8✔
125
            this._dec = await this._builder.buildWithStreamAsync(
8✔
126
              this._readCbk.bind(this),
8✔
127
              null,
8✔
128
              null,
8✔
129
              null,
8✔
130
              null,
8✔
131
              this._writeCbk.bind(this),
8✔
132
              this._metadataCbk.bind(this),
8✔
133
              this._errorCbk.bind(this),
8✔
134
            )
8✔
135
          }
8✔
136
        } catch (error) {
11!
137
          const initStatus = error.status || -1
×
UNCOV
138
          const initStatusString = error.statusString || 'Unknown'
×
UNCOV
139
          this._debug(`Failed initializing decoder: ${initStatus} ${initStatusString}`)
×
UNCOV
140
          throw error
×
UNCOV
141
        }
×
142
      }
11✔
143

144
      this._debug(`Received ${chunk.length} bytes to process`)
142✔
145
      this._chunks.push(chunk)
142✔
146
      while (!this._chunksIsAlmostEmpty() && this._dec !== null) {
142✔
147
        // a processSingle needs data, and have received enough, so it is time to unblock
530✔
148
        if (this._fillUpPause) {
530!
149
          this._debug('Another processSingle call is blocked and there is enough data -> unblocking')
×
UNCOV
150
          this._fillUpPause()
×
UNCOV
151
          callback()
×
152
          return
×
153
        }
×
154
        // if the processSingle blocked did not finished yet, just don't do anything
530✔
155
        if (this._readCallback) {
530!
UNCOV
156
          this._debug('Another processSingle call did not finished yet, doing nothing with this')
×
UNCOV
157
          callback()
×
UNCOV
158
          return
×
UNCOV
159
        }
×
160

161
        // flag to know if the next processSingle call has been blocked
530✔
162
        this._hasBeenBlocked = false
530✔
163
        this._lastBytesDemanded = 0
530✔
164
        // store the current callback just in case it gets blocked
530✔
165
        this._readCallback = callback
530✔
166
        this._debug('Processing data')
530✔
167
        if (!(await this._dec.processSingleAsync())) {
530!
UNCOV
168
          this._throwDecoderError()
×
UNCOV
169
          return
×
UNCOV
170
        }
×
171
        this._readCallback = null
530✔
172

173
        // if chained is enabled and the decoder is in end of link, then finish link
530✔
174
        // and prepare the decoder for the next link
530✔
175
        if (this._dec.getState() === flac.Decoder.State.END_OF_LINK) {
530✔
176
          this._debug('End of link reached')
2✔
177
          await this._dec.finishLinkAsync()
2✔
178
          this.emit('end-link')
2✔
179
        }
2✔
180

181
        // if the call has been blocked, then it does not need to do anything else
530✔
182
        if (this._hasBeenBlocked) {
530!
UNCOV
183
          this._debug('processSingle was blocked, stopping loop for this processing')
×
UNCOV
184
          return
×
UNCOV
185
        }
×
186

187
        // avoid values of 0 bytes, by using a default value
530✔
188
        this._lastBytesDemanded = this._lastBytesDemanded || this._dec.getBlocksize() || 1024 * 32
530✔
189
      }
530✔
190
      callback()
142✔
191
    } catch (e) {
142!
UNCOV
192
      callback(e)
×
UNCOV
193
    }
×
194
  }
142✔
195

196
  async _flush(callback) {
1✔
197
    try {
12✔
198
      if (!this._dec) {
12✔
199
        this._debug('Decoder did not receive any data and it is being finalized')
1✔
200
        callback(null)
1✔
201
        return
1✔
202
      }
1✔
203

204
      this._timeToDie = true
11✔
205
      if (this._chunks.length > 0) {
11✔
206
        this._debug('Processing final chunks of data')
11✔
207
        if (!(await this._dec.processUntilEndOfStreamAsync())) {
11!
208
          this._throwDecoderError()
×
209
          return
×
UNCOV
210
        }
×
211
      }
11✔
212

213
      this._debug('Flushing decoder')
11✔
214
      if (!(await this._dec.finishAsync())) {
12!
UNCOV
215
        this._throwDecoderError()
×
UNCOV
216
      }
✔
217

218
      callback(null)
11✔
219
    } catch (e) {
12!
UNCOV
220
      callback(e)
×
UNCOV
221
    }
×
222
  }
12✔
223

224
  _writeCbk(frame, buffers) {
1✔
225
    const outBps = this.getOutputBitsPerSample() / 8
533✔
226
    const buff = flac.fns.zipAudio({ samples: frame.header.blocksize, outBps, buffers })
533✔
227

228
    this._processedSamples += frame.header.blocksize
533✔
229
    this.push(buff)
533✔
230
    this._debug(`Received ${frame.header.blocksize} samples (${buff.length} bytes) of decoded data`)
533✔
231

232
    return flac.Decoder.WriteStatus.CONTINUE
533✔
233
  }
533✔
234

235
  _readCbk(buffer) {
1✔
236
    if (this._chunks.length > 0) {
1,099✔
237
      const b = this._chunks[0]
1,090✔
238
      const bytesRead = b.copy(buffer, 0)
1,090✔
239
      if (bytesRead === b.length) {
1,090✔
240
        this._chunks = this._chunks.slice(1)
142✔
241
      } else {
1,090✔
242
        this._chunks[0] = b.slice(bytesRead)
948✔
243
      }
948✔
244
      this._lastBytesDemanded += buffer.length
1,090✔
245
      this._debug(`Read ${bytesRead} bytes from stored chunks to be decoded`)
1,090✔
246
      return { bytes: bytesRead, returnValue: flac.Decoder.ReadStatus.CONTINUE }
1,090✔
247
    }
1,090✔
248

249
    if (this._timeToDie) {
9✔
250
      this._debug('Wanted to read, but the stream is being finished -> returning EOF')
9✔
251
      return { bytes: 0, returnValue: flac.Decoder.ReadStatus.END_OF_STREAM }
9✔
252
    }
9!
253

254
    this._debug(`There is no chunks of data to be read (wanted ${buffer.length}) -> blocking processSingle`)
×
255
    // there is not enough data: block the processSingle
×
UNCOV
256
    //   1. store the required bytes
×
257
    this._lastBytesDemanded = buffer.length
×
258
    //   2. copy the callback
×
259
    const cbk = this._readCallback
×
260
    //   3. notify that processSingle has been blocked
×
261
    this._hasBeenBlocked = true
×
UNCOV
262
    //   4. block
×
UNCOV
263
    return new Promise((resolve) => {
×
UNCOV
264
      this._fillUpPause = () => {
×
UNCOV
265
        resolve({ bytes: 0, returnValue: flac.Decoder.ReadStatus.CONTINUE })
×
UNCOV
266
        this._fillUpPause = undefined
×
UNCOV
267
      }
×
268

UNCOV
269
      if (cbk) {
×
UNCOV
270
        // call the readCallback here
×
UNCOV
271
        cbk()
×
UNCOV
272
      }
×
UNCOV
273
    })
×
274
  }
1,099✔
275

276
  _metadataCbk(metadata) {
1✔
277
    if (metadata.type === flac.format.MetadataType.STREAMINFO) {
16✔
278
      this.emit('format', {
13✔
279
        channels: metadata.channels,
13✔
280
        bitDepth: metadata.bitsPerSample,
13✔
281
        bitsPerSample: metadata.bitsPerSample,
13✔
282
        is32bit: this._outputAs32,
13✔
283
        sampleRate: metadata.sampleRate,
13✔
284
        totalSamples: metadata.totalSamples,
13✔
285
      })
13✔
286
      this._totalSamples = metadata.totalSamples
13✔
287
      this._channels = metadata.channels
13✔
288
      this._bitsPerSample = metadata.bitsPerSample
13✔
289
      this._sampleRate = metadata.sampleRate
13✔
290
    }
13✔
291

292
    this.emit('metadata', metadata)
16✔
293
  }
16✔
294

295
  _errorCbk(code) {
1✔
UNCOV
296
    const message = flac.Decoder.ErrorStatusString[code]
×
UNCOV
297
    this._debug(`Decoder called error callback ${code} (${message})`)
×
UNCOV
298
    this.emit('flac-error', { code, message })
×
UNCOV
299
  }
×
300

301
  _throwDecoderError() {
1✔
UNCOV
302
    const error = this._dec.getState()
×
UNCOV
303
    const errorObj = new Error(this._dec.getResolvedStateString())
×
UNCOV
304
    this._debug(`Decoder call failed: ${this._dec.getResolvedStateString()} [${error}]`)
×
UNCOV
305
    errorObj.code = error
×
UNCOV
306
    throw errorObj
×
UNCOV
307
  }
×
308

309
  _chunksIsAlmostEmpty() {
1✔
310
    if (this._lastBytesDemanded !== undefined) {
672✔
311
      return this._chunks.reduce((r, v) => r + v.length, 0) < this._lastBytesDemanded * 2
650✔
312
    }
650✔
313
    return this._chunks.length < 2
22✔
314
  }
672✔
315
}
1✔
316

317
export default StreamDecoder
1✔
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