@babblevoice/projectrtp 2.4.17 → 2.5.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,657 @@
1
+ /* if we want to see what is going on - use nodeplotlib instead of our placeholder */
2
+ //const npl = require( "nodeplotlib" )
3
+ // eslint-disable-next-line no-unused-vars
4
+ const npl = { plot: ( /** @type {any} */ a ) => {} }
5
+
6
+
7
+ const fft = require( "fft-js" ).fft
8
+ const projectrtp = require( "../../index" ).projectrtp
9
+ const expect = require( "chai" ).expect
10
+ const dgram = require( "dgram" )
11
+ const fs = require( "fs" )
12
+
13
+
14
+ /*
15
+ So that we do not have to impliment g722 or other codecs in JS, we create 2 channels, and mix them.
16
+ On one end, we UDP echo back - which means, for example, g722 will be echoed back, then on the other end,
17
+ we generate a signal (tone) and check we receive that signal on the end end.
18
+ */
19
+
20
+ const datalength = 8192 /* 1 second of data */
21
+ const frequency = 400
22
+
23
+
24
+ const magnitude = ( Math.pow( 2, 16 ) / 2 ) - ( 65536 / 4 )
25
+
26
+ /**
27
+ * Generates time series signal with one sinewave component @ hz
28
+ * @param { number } hz
29
+ * @returns { Int16Array }
30
+ */
31
+ function gensignal( hz ) {
32
+
33
+ const y = new Int16Array( datalength )
34
+
35
+ for( let i = 0; i < datalength; i ++ ) {
36
+ y[ i ] = Math.sin( i * ( Math.PI * 2 * ( 1 / 8000 ) ) * hz ) * magnitude
37
+ }
38
+
39
+ /*
40
+ npl.plot( [ {
41
+ y: Array.from( y ),
42
+ type: "scatter"
43
+ } ] )
44
+ */
45
+
46
+ return y
47
+ }
48
+
49
+ /**
50
+ *
51
+ * @param { Array } arr
52
+ * @returns
53
+ */
54
+ function truncatetopoweroftwo( arr ) {
55
+ const newsize = Math.pow( 2, Math.floor( Math.log2( arr.length ) ) )
56
+ return arr.slice( 0, newsize )
57
+ }
58
+
59
+ /**
60
+ *
61
+ * @param { Array< Array< number > > } c - array of complex numbers as returned by fft
62
+ * @returns { Array< number > }
63
+ */
64
+ function amplitude( c ) {
65
+ const out = []
66
+
67
+ for( let k = 0; k < c.length; k++ ) {
68
+ const complex = c[ k ]
69
+ const r = complex[ 0 ]
70
+ const i = complex[ 1 ]
71
+ out.push( Math.sqrt( ( r * r ) + ( i * i ) ) )
72
+ }
73
+
74
+ return out
75
+ }
76
+
77
+ /**
78
+ *
79
+ * @param { Array< number > } inarr
80
+ * @param { number } startpos
81
+ * @param { number } endpos
82
+ */
83
+ function sum( inarr, startpos, endpos ) {
84
+ let oursum = 0
85
+ for( let i = startpos; i < endpos; i++ ) oursum += inarr[ i ]
86
+ return oursum
87
+ }
88
+
89
+ /**
90
+ *
91
+ * @param { Int16Array } signal
92
+ * @returns { Array< number > }
93
+ */
94
+ function ampbyfrequency( signal ) {
95
+ const pow2signal = truncatetopoweroftwo( Array.from( signal ) )
96
+ const ourfft = fft( pow2signal )
97
+ const amps = amplitude( ourfft )
98
+
99
+ /*
100
+ npl.plot( [ {
101
+ y: amps
102
+ } ] )
103
+ */
104
+
105
+ return amps
106
+ }
107
+
108
+ /**
109
+ * Checks fft of signal to see if we have a signal at hz present
110
+ * @param { Array< number > } amps
111
+ * @param { number } hz
112
+ * @param { number } threshold
113
+ */
114
+ function has( amps , hz, threshold ) {
115
+ return sum( amps, hz - 20, hz + 20 ) > threshold
116
+ }
117
+
118
+ /**
119
+ *
120
+ * @param { Array< number > } inarray
121
+ * @returns { Array< number > }
122
+ */
123
+ function lineartopcma( inarray ) {
124
+ const out = []
125
+ for( let i = 0; i < inarray.length; i++ )
126
+ out.push( projectrtp.codecx.linear162pcma( inarray[ i ] ) )
127
+ return out
128
+ }
129
+
130
+ /**
131
+ *
132
+ * @param { Array< number > } inarray
133
+ * @returns { Int16Array }
134
+ */
135
+ function pcmatolinear( inarray ) {
136
+ const out = new Int16Array( inarray.length )
137
+ for( let i = 0; i < inarray.length; i++ ) {
138
+ out[ i ] = projectrtp.codecx.pcma2linear16( inarray[ i ] )
139
+ }
140
+
141
+ return out
142
+ }
143
+
144
+ /**
145
+ *
146
+ * @param { Array< number > } inarray
147
+ * @returns { Array< number > }
148
+ */
149
+ function lineartopcmu( inarray ) {
150
+ const out = []
151
+ for( let i = 0; i < inarray.length; i++ )
152
+ out.push( projectrtp.codecx.linear162pcmu( inarray[ i ] ) )
153
+ return out
154
+ }
155
+
156
+ /**
157
+ *
158
+ * @param { Array< number > } inarray
159
+ * @returns { Int16Array }
160
+ */
161
+ function pcmutolinear( inarray ) {
162
+ const out = new Int16Array( inarray.length )
163
+ for( let i = 0; i < inarray.length; i++ ) {
164
+ out[ i ] = projectrtp.codecx.pcmu2linear16( inarray[ i ] )
165
+ }
166
+
167
+ return out
168
+ }
169
+
170
+ /**
171
+ *
172
+ * @param { number } sn - should start from 0 which we use to index into the supplied data buffer
173
+ * @param { number } dstport
174
+ * @param { object } server
175
+ * @param { number } pt - payload type
176
+ * @param { number } ssrc - a unique payload type
177
+ * @param { Array< number > } payload
178
+ * @param { number } [ snoffset = 0 ] - if we want to have an offset
179
+ * @param { function } [ cb ] - callback when sent
180
+ * @returns
181
+ */
182
+ function sendpk( sn, dstport, server, pt = 0, ssrc, payload, snoffset=0, cb ) {
183
+
184
+ if( !ssrc ) ssrc = 25
185
+ const ts = sn * 160
186
+ const sendtime = sn * 20
187
+
188
+ const uint8pl = new Uint8Array( payload.slice( sn , sn + 160 ) )
189
+
190
+ return setTimeout( () => {
191
+ const subheader = Buffer.alloc( 10 )
192
+
193
+ subheader.writeUInt16BE( ( sn + snoffset ) % ( 2**16 ) )
194
+ subheader.writeUInt32BE( ts, 2 )
195
+ subheader.writeUInt32BE( ssrc, 6 )
196
+
197
+ const rtppacket = Buffer.concat( [
198
+ Buffer.from( [ 0x80, pt ] ),
199
+ subheader,
200
+ uint8pl ] )
201
+
202
+ server.send( rtppacket, dstport, "localhost" )
203
+ if( cb ) cb( { rtppacket, dstport } )
204
+ }, sendtime )
205
+ }
206
+
207
+ /**
208
+ * Limitation of not parsing ccrc.
209
+ * @param { Buffer } packet
210
+ * @return { object }
211
+ */
212
+ function parsepk( packet ) {
213
+ return {
214
+ sn: packet.readUInt16BE( 2 ),
215
+ ts: packet.readUInt32BE( 4 ),
216
+ pt: packet.readUInt8( 1 ) & 0x7f,
217
+ ssrc: packet.readUInt32BE( 8 ),
218
+ payload: new Uint8Array( packet.slice( 12 ) )
219
+ }
220
+ }
221
+
222
+ /**
223
+ * @callback encodefunction
224
+ * @param { Array< number > } inarray
225
+ * @returns { Array< number > }
226
+ */
227
+
228
+ /**
229
+ * @callback decodefunction
230
+ * @param { Array< number > } inarray
231
+ * @returns { Int16Array }
232
+ */
233
+
234
+ /**
235
+ * Run a loop test: generate signal - endcode pass to a channel, mix with second channal
236
+ * receive this rtp and loop back and finally recieve and test for signal in sound.
237
+ * This tests the full audio loop with codec conversion.
238
+ * The encode and decode functions must match the bcodec, i.e. bcodec tells
239
+ * projectrtp what codec to accept on that channel, the functions are what takes
240
+ * our linear16 and encodes and decodes into the payload.
241
+ * @param { number } acodec
242
+ * @param { number } bcodec
243
+ * @param { encodefunction } encode
244
+ * @param { decodefunction } decode
245
+ * @param { number } [ ilbcpt = -1 ] if acodec is ilbc then set the dynamic pt
246
+ */
247
+ async function looptest( acodec, bcodec, encode, decode, ilbcpt = -1 ) {
248
+ const a = dgram.createSocket( "udp4" )
249
+ const b = dgram.createSocket( "udp4" )
250
+
251
+ a.bind()
252
+ await new Promise( resolve => a.on( "listening", resolve ) )
253
+ b.bind()
254
+ await new Promise( resolve => b.on( "listening", resolve ) )
255
+
256
+ let done
257
+ const finished = new Promise( ( r ) => { done = r } )
258
+
259
+ const channeladef = { "id": "4", "remote": { "address": "localhost", "port": a.address().port, "codec": acodec } }
260
+ if( 97 == acodec && -1 != ilbcpt ) channeladef.remote.ilbcpt = ilbcpt
261
+
262
+ const achannel = await projectrtp.openchannel( channeladef, function( d ) {
263
+ if( "close" === d.action ) {
264
+ a.close()
265
+ b.close()
266
+ bchannel.close()
267
+ }
268
+ } )
269
+
270
+ const bchannel = await projectrtp.openchannel( { "id": "4", "remote": { "address": "localhost", "port": b.address().port, "codec": bcodec } }, function( d ) {
271
+ if( "close" === d.action ) done()
272
+ } )
273
+
274
+ bchannel.mix( achannel )
275
+
276
+ /* echo straight back */
277
+ a.on( "message", function( msg ) {
278
+ const rtppk = parsepk( msg )
279
+ if( -1 != ilbcpt ) expect( rtppk.pt ).to.equal( ilbcpt )
280
+ a.send( msg, achannel.local.port, "localhost" )
281
+ } )
282
+
283
+ let received = Buffer.alloc( 0 )
284
+ let ondonereceiving, recvcount = 0
285
+ const receiveuntil = new Promise( resolve => ondonereceiving = resolve )
286
+ b.on( "message", function( msg ) {
287
+ const pk = parsepk( msg )
288
+ received = Buffer.concat( [ received, pk.payload ] )
289
+ if( 50 < recvcount++ ) ondonereceiving()
290
+ } )
291
+
292
+ const y = gensignal( frequency )
293
+ const encoded = encode( Array.from( y ) )
294
+
295
+ for( let i = 0; 60 > i; i ++ ) {
296
+ sendpk( i, bchannel.local.port, b, bcodec, 44, encoded )
297
+ }
298
+
299
+ await receiveuntil
300
+ const y2 = decode( Array.from( received ) )
301
+
302
+ achannel.close()
303
+ await finished
304
+
305
+ npl.plot( [ {
306
+ y: Array.from( y ),
307
+ type: "scatter"
308
+ } ] )
309
+
310
+ npl.plot( [ {
311
+ y: Array.from( y2 ),
312
+ type: "scatter"
313
+ } ] )
314
+
315
+ const amps = ampbyfrequency( y2 )
316
+ expect( has( amps, frequency - 100, 25000000 ) ).to.be.false
317
+ expect( has( amps, frequency, 25000000 ) ).to.be.true
318
+ expect( has( amps, frequency + 100, 25000000 ) ).to.be.false
319
+ }
320
+
321
+ /**
322
+ * Test to check we receive all packets. ALso check basic codecs to make sure
323
+ * no duff packets come through (i,.e. memory is cleared out). It will only work
324
+ * with non-lossy CODECS
325
+ * @param { number } acodec
326
+ * @param { number } bcodec
327
+ * @param { encodefunction } encode
328
+ * @param { decodefunction } decode
329
+ * @param { number } [ expectedval ] what value we expect after the round trip (i.e. ulaw - alawy and back again might not be the same value)
330
+ */
331
+ async function loopcounttest( acodec, bcodec, encode, decode, expectedval = 0 ) {
332
+
333
+ const a = dgram.createSocket( "udp4" )
334
+ const b = dgram.createSocket( "udp4" )
335
+
336
+ a.bind()
337
+ await new Promise( resolve => a.on( "listening", resolve ) )
338
+ b.bind()
339
+ await new Promise( resolve => b.on( "listening", resolve ) )
340
+
341
+ let done
342
+ const finished = new Promise( ( r ) => { done = r } )
343
+
344
+ const allstats = {
345
+ a: {
346
+ recv:{ count: 0 },
347
+ send:{ count: 0 },
348
+ port: a.address().port
349
+ },
350
+ b: {
351
+ recv:{ count: 0 },
352
+ send:{ count: 0 },
353
+ srcport: b.address().port,
354
+ dstport: 0
355
+ },
356
+ notcorrect: 0
357
+ }
358
+
359
+ const achannel = await projectrtp.openchannel( { "id": "4", "remote": { "address": "localhost", "port": a.address().port, "codec": acodec } }, function( d ) {
360
+ if( "close" === d.action ) {
361
+ a.close()
362
+ b.close()
363
+ bchannel.close()
364
+ allstats.achannel = { stats: d.stats }
365
+ }
366
+ } )
367
+
368
+ const bchannel = await projectrtp.openchannel( { "id": "4", "remote": { "address": "localhost", "port": b.address().port, "codec": bcodec } }, function( d ) {
369
+ if( "close" === d.action ) {
370
+ allstats.bchannel = { stats: d.stats }
371
+ done()
372
+ }
373
+ } )
374
+
375
+ allstats.b.dstport = bchannel.local.port
376
+
377
+ /* echo straight back */
378
+ a.on( "message", function( msg ) {
379
+ a.send( msg, achannel.local.port, "localhost" )
380
+ allstats.a.recv.count++
381
+ allstats.a.send.count++
382
+ } )
383
+
384
+ bchannel.mix( achannel )
385
+
386
+ b.on( "message", function( msg ) {
387
+ allstats.b.recv.count++
388
+ const pk = parsepk( msg )
389
+ const decoded = decode( Array.from( pk.payload ) )
390
+ for( let i = 0; i < decoded.length; i++ ) {
391
+ if( expectedval != decoded[ i ] ) allstats.notcorrect++
392
+ }
393
+ } )
394
+
395
+ const y = new Int16Array( datalength ).fill( 0 )
396
+ const encoded = encode( Array.from( y ) )
397
+
398
+ for( let i = 0; 60 > i; i ++ ) {
399
+ sendpk( i, bchannel.local.port, b, bcodec, 44, encoded, 0, () => { allstats.b.send.count++ } )
400
+ }
401
+
402
+ const bufferdelay = 350
403
+ const errormarin = 500
404
+ const packettime = 20 * 60
405
+ const totaltimerequired = packettime + bufferdelay + errormarin
406
+ await new Promise( resolve => setTimeout( resolve, totaltimerequired ) )
407
+
408
+ achannel.close()
409
+ await finished
410
+
411
+ return allstats
412
+ }
413
+
414
+
415
+
416
+ describe( "Transcode", function() {
417
+
418
+ this.slow( 3000 )
419
+ this.timeout( 5000 )
420
+
421
+ it( "basic count test and data check trancode pcmu <==> pcmu", async function() {
422
+ /* 2 seconds is important, it should be below 60 * 20mS + JT = 1200 + 300 + 300 = 1800mS - we are taking 1820 */
423
+ this.timeout( 4000 )
424
+ this.slow( 2000 )
425
+
426
+ const all = []
427
+ for( let i = 0; 50 > i; i++) {
428
+ all.push( loopcounttest( 0, 0, lineartopcmu, pcmutolinear ) )
429
+ }
430
+ const results = await Promise.all( all )
431
+ results.forEach( ( i ) => {
432
+ expect( i.a.recv.count ).to.equal( 60 )
433
+ expect( i.b.recv.count ).to.equal( 60 )
434
+ } )
435
+ } )
436
+
437
+ it( "basic count test and data check trancode pcma <==> pcma", async function() {
438
+ this.timeout( 3000 )
439
+ this.slow( 2500 )
440
+ const result = await loopcounttest( 0, 0, lineartopcma, pcmatolinear, 8 )
441
+ expect( result.notcorrect ).to.equal( 0 )
442
+ } )
443
+
444
+ it( "basic count test and data check trancode pcmu <==> pcma", async function() {
445
+ this.timeout( 3000 )
446
+ this.slow( 2500 )
447
+ const result = await loopcounttest( 8, 0, lineartopcmu, pcmutolinear, 8 )
448
+ expect( result.notcorrect ).to.equal( 0 )
449
+ } )
450
+
451
+ it( "basic count test and data check trancode pcma <==> pcmu", async function() {
452
+ this.timeout( 3000 )
453
+ this.slow( 2500 )
454
+ const result = await loopcounttest( 0, 8, lineartopcma, pcmatolinear, 8 )
455
+ expect( result.notcorrect ).to.equal( 0 )
456
+ } )
457
+
458
+ it( "Test our linear to pcma converting routines", async function() {
459
+
460
+ const y = gensignal( frequency )
461
+
462
+ const pcma = lineartopcma( Array.from( y ) )
463
+ const y2 = pcmatolinear( pcma )
464
+
465
+ npl.plot( [ {
466
+ y: y2,
467
+ type: "scatter"
468
+ } ] )
469
+
470
+ const amps = ampbyfrequency( y )
471
+ expect( has( amps, 300, 25000000 ) ).to.be.false
472
+ expect( has( amps, 400, 25000000 ) ).to.be.true
473
+ expect( has( amps, 500, 25000000 ) ).to.be.false
474
+ } )
475
+
476
+ it( "trancode pcmu <==> ilbc static pt", async function() {
477
+ await looptest( 97, 0, lineartopcmu, pcmutolinear )
478
+ } )
479
+
480
+ it( "trancode pcmu <==> ilbc with dynamic pt", async function() {
481
+ await looptest( 97, 0, lineartopcmu, pcmutolinear, 123 )
482
+ } )
483
+
484
+ it( "trancode pcmu <==> g722", async function() {
485
+ await looptest( 9, 0, lineartopcmu, pcmutolinear )
486
+ } )
487
+
488
+ it( "trancode pcmu <==> pcma", async function() {
489
+ await looptest( 8, 0, lineartopcmu, pcmutolinear )
490
+ } )
491
+
492
+ it( "trancode pcma <==> ilbc", async function() {
493
+ await looptest( 97, 8, lineartopcma, pcmatolinear )
494
+ } )
495
+
496
+ it( "trancode pcma <==> g722", async function() {
497
+ await looptest( 9, 8, lineartopcma, pcmatolinear )
498
+ } )
499
+
500
+ it( "trancode pcma <==> pcmu", async function() {
501
+ await looptest( 0, 8, lineartopcma, pcmatolinear )
502
+ } )
503
+
504
+ it( "trancode pcma <==> pcma", async function() {
505
+ await looptest( 8, 8, lineartopcma, pcmatolinear )
506
+ } )
507
+
508
+ it( "trancode pcmu <==> pcmu", async function() {
509
+ await looptest( 0, 0, lineartopcmu, pcmutolinear )
510
+ } )
511
+
512
+ it( "simulate an xfer with multiple mix then test new path pcma <==> g722", async function() {
513
+
514
+ this.timeout( 8000 )
515
+ this.slow( 7000 )
516
+
517
+ /* make sure we have some tone to play */
518
+ projectrtp.tone.generate( "300*0.5:2000", "/tmp/tone.wav" )
519
+ projectrtp.tone.generate( "800*0.5:2000", "/tmp/hightone.wav" )
520
+
521
+ /**
522
+ * a and b is the 2 phone legs, c is the transfered channel
523
+ */
524
+ const acodec = 8
525
+ const bcodec = 0
526
+ const ccodec = 9
527
+
528
+ /*
529
+ a = 8, b = 0, c = 0 missing tones on a
530
+ a = 8, b = 9, c = 0 all works
531
+ a = 8, b = 0, c = 9, missing tones and missing c leg back on a
532
+ */
533
+
534
+ const a = dgram.createSocket( "udp4" )
535
+ const b = dgram.createSocket( "udp4" )
536
+ const c = dgram.createSocket( "udp4" )
537
+
538
+ a.bind()
539
+ await new Promise( resolve => a.on( "listening", resolve ) )
540
+ b.bind()
541
+ await new Promise( resolve => b.on( "listening", resolve ) )
542
+ c.bind()
543
+ await new Promise( resolve => c.on( "listening", resolve ) )
544
+
545
+ /* echo straight back */
546
+ c.on( "message", function( msg ) {
547
+ c.send( msg, cchannel.local.port, "localhost" )
548
+ } )
549
+
550
+ b.on( "message", function( msg ) {
551
+ b.send( msg, bchannel.local.port, "localhost" )
552
+ } )
553
+
554
+ let received = Buffer.alloc( 0 )
555
+ let ondonereceiving, recvcount = 0
556
+ const receiveuntil = new Promise( resolve => ondonereceiving = resolve )
557
+ a.on( "message", function( msg ) {
558
+ const pk = parsepk( msg )
559
+ received = Buffer.concat( [ received, pk.payload ] )
560
+ if( 100 < recvcount++ ) ondonereceiving()
561
+ } )
562
+
563
+ let done
564
+ const finished = new Promise( ( resolve ) => { done = resolve } )
565
+
566
+ let unmixresolve
567
+ const unmixdone = new Promise( resolve => unmixresolve = resolve )
568
+
569
+ /* This channel reflects the outbound channel */
570
+ const achannel = await projectrtp.openchannel( { "id": "4" }, function( d ) {
571
+ if( "close" === d.action ) {
572
+ a.close()
573
+ b.close()
574
+ c.close()
575
+ cchannel.close()
576
+ }
577
+
578
+ if( "mix" === d.action && "finished" === d.event ) unmixresolve()
579
+ } )
580
+
581
+ await new Promise( resolve => setTimeout( resolve, 800 ) )
582
+ achannel.remote( { "address": "localhost", "port": a.address().port, "codec": acodec } )
583
+
584
+ /* This channel reflects the originator */
585
+ const bchannel = await projectrtp.openchannel( { "id": "5", "remote": { "address": "localhost", "port": b.address().port, "codec": bcodec } }, function( /*d*/ ) {
586
+ } )
587
+
588
+ achannel.mix( bchannel )
589
+ await new Promise( resolve => setTimeout( resolve, 500 ) ) // we really should wait for the mix start events
590
+
591
+ /* for some reason in our lib this gets sent again */
592
+ achannel.remote( { "address": "localhost", "port": a.address().port, "codec": acodec } )
593
+ bchannel.remote( { "address": "localhost", "port": b.address().port, "codec": bcodec } )
594
+
595
+ achannel.mix( bchannel )
596
+ bchannel.record( { file: "/tmp/test.wav", numchannels: 2, mp3: true } )
597
+
598
+ const y = gensignal( 100 )
599
+ const encoded = lineartopcma( Array.from( y ) )
600
+
601
+ for( let i = 0 ; 120 > i; i ++ ) {
602
+ sendpk( i, achannel.local.port, a, acodec, 44, encoded, 6300 )
603
+ }
604
+
605
+ await new Promise( resolve => setTimeout( resolve, 1200 ) )
606
+
607
+ /* Now our blind xfer happens */
608
+ achannel.unmix()
609
+ bchannel.unmix()
610
+ await unmixdone
611
+
612
+ /* moh followed by ringing tone */
613
+ achannel.play( { "loop": true, "files": [ { "wav": "/tmp/tone.wav" } ] } )
614
+ await new Promise( resolve => setTimeout( resolve, 200 ) )
615
+ achannel.play( { "loop": true, "files": [ { "wav": "/tmp/hightone.wav" } ] } )
616
+
617
+ await new Promise( resolve => setTimeout( resolve, 200 ) )
618
+
619
+ /* now open our new leg */
620
+ const cchannel = await projectrtp.openchannel( { "id": "6" }, function( d ) {
621
+ if( "close" === d.action ) done()
622
+ } )
623
+
624
+ bchannel.close()
625
+ cchannel.remote( { "address": "localhost", "port": c.address().port, "codec": ccodec } )
626
+ await new Promise( resolve => setTimeout( resolve, 40 ) )
627
+ cchannel.mix( achannel )
628
+
629
+ /* now we have one way audio in real life */
630
+ await receiveuntil
631
+ const y2 = pcmatolinear( Array.from( received ) )
632
+
633
+ await new Promise( resolve => setTimeout( resolve, 1000 ) )
634
+ achannel.close()
635
+ await finished
636
+
637
+ npl.plot( [ {
638
+ y: Array.from( y ),
639
+ type: "scatter"
640
+ } ] )
641
+
642
+ npl.plot( [ {
643
+ y: Array.from( y2 ),
644
+ type: "scatter"
645
+ } ] )
646
+
647
+ /* TODO this currently doesn't test the c leg as this is teh same frequency as the a leg*/
648
+ const amps = ampbyfrequency( y2 )
649
+ expect( has( amps, 100, 25000000 ) ).to.be.true
650
+ expect( has( amps, 300, 25000000 ) ).to.be.true
651
+ expect( has( amps, 800, 25000000 ) ).to.be.true
652
+ expect( has( amps, 500, 25000000 ) ).to.be.false
653
+
654
+ await fs.promises.unlink( "/tmp/ukringing.wav" ).catch( () => {} )
655
+ } )
656
+ } )
657
+