@babblevoice/projectrtp
Version: 
A scalable Node addon RTP server
822 lines (659 loc) • 23.9 kB
JavaScript
/* if we want to see what is going on - use nodeplotlib instead of our placeholder */
//const npl = require( "nodeplotlib" )
// eslint-disable-next-line no-unused-vars
const npl = { plot: ( /** @type {any} */ a ) => {} }
const fft = require( "fft-js" ).fft
const projectrtp = require( "../../index" ).projectrtp
const expect = require( "chai" ).expect
const dgram = require( "dgram" )
const fs = require( "fs" )
const pcap = require( "./pcap" )
/*
So that we do not have to impliment g722 or other codecs in JS, we create 2 channels, and mix them. 
On one end, we UDP echo back - which means, for example, g722 will be echoed back, then on the other end, 
we generate a signal (tone) and check we receive that signal on the end end.
*/
const datalength = 8192 /* 1 second of data */
const frequency = 400
const magnitude = ( Math.pow( 2, 16 ) / 2 ) - ( 65536 / 4 )
/**
 * Generates time series signal with one sinewave component @ hz
 * @param { number } hz 
 * @returns { Int16Array }
 */
function gensignal( hz ) {
  const y = new Int16Array( datalength )
  for( let i = 0; i < datalength; i ++ ) {
    y[ i ] = Math.sin( i * ( Math.PI * 2 * ( 1 / 8000 ) ) * hz ) * magnitude
  }
  /*
  npl.plot( [ {
    y: Array.from( y ),
    type: "scatter"
  } ] )
  */
  return y
}
/**
 * 
 * @param { Array } arr 
 * @returns 
 */
function truncatetopoweroftwo( arr ) {
  const newsize = Math.pow( 2, Math.floor( Math.log2( arr.length ) ) )
  return arr.slice( 0, newsize )
}
/**
 * 
 * @param { Array< Array< number > > } c - array of complex numbers as returned by fft 
 * @returns { Array< number > }
 */
function amplitude( c ) {
  const out = []
  for( let k = 0; k < c.length; k++ ) {
    const complex = c[ k ]
    const r = complex[ 0 ]
    const i = complex[ 1 ]
    out.push( Math.sqrt( ( r * r ) + ( i * i ) ) )
  }
  return out
}
/**
 * 
 * @param { Array< number > } inarr
 * @param { number } startpos 
 * @param { number } endpos 
 */
function sum( inarr, startpos, endpos ) {
  let oursum = 0
  for( let i = startpos; i < endpos; i++ ) oursum += inarr[ i ]
  return oursum
}
/**
 * 
 * @param { Int16Array } signal 
 * @returns { Array< number > }
 */
function ampbyfrequency( signal ) {
  const pow2signal = truncatetopoweroftwo( Array.from( signal ) )
  const ourfft = fft( pow2signal )
  const amps = amplitude( ourfft )
  /*
  npl.plot( [ {
    y: amps
  } ] )
  */
  return amps
}
/**
 * Checks fft of signal to see if we have a signal at hz present
 * @param { Array< number > } amps
 * @param { number } hz 
 * @param { number } threshold
 */
function has( amps , hz, threshold ) {
  return sum( amps, hz - 20, hz + 20 ) > threshold
}
/**
 * 
 * @param { Array< number > } inarray
 * @returns { Array< number > }
 */
function lineartopcma( inarray ) {
  const out = []
  for( let i = 0; i < inarray.length; i++ )
    out.push( projectrtp.codecx.linear162pcma( inarray[ i ] ) )
  return out
}
/**
 * 
 * @param { Array< number > } inarray
 * @returns { Int16Array }
 */
function pcmatolinear( inarray ) {
  const out = new Int16Array( inarray.length )
  for( let i = 0; i < inarray.length; i++ ) {
    out[ i ] = projectrtp.codecx.pcma2linear16( inarray[ i ] )
  }
  return out
}
/**
 * 
 * @param { Array< number > } inarray
 * @returns { Array< number > }
 */
function lineartopcmu( inarray ) {
  const out = []
  for( let i = 0; i < inarray.length; i++ )
    out.push( projectrtp.codecx.linear162pcmu( inarray[ i ] ) )
  return out
}
/**
 * 
 * @param { Array< number > } inarray
 * @returns { Int16Array }
 */
function pcmutolinear( inarray ) {
  const out = new Int16Array( inarray.length )
  for( let i = 0; i < inarray.length; i++ ) {
    out[ i ] = projectrtp.codecx.pcmu2linear16( inarray[ i ] )
  }
  return out
}
/**
 * Send Buffer to server at required time
 * @param { number } sendtime 
 * @param { Buffer } pk 
 * @param { number } dstport 
 * @param { dgram.Socket } server 
 * @returns 
 */
function sendpayload( sendtime, pk, dstport, server ) {
  return setTimeout( () => {
    server.send( pk, dstport, "localhost" )
  }, sendtime )
}
/**
 * 
 * @param { number } sn - should start from 0 which we use to index into the supplied data buffer
 * @param { number } dstport 
 * @param { object } server 
 * @param { number } pt - payload type
 * @param { number } ssrc  - a unique payload type
 * @param { Array< number > } payload
 * @param { number } [ snoffset = 0 ] - if we want to have an offset
 * @param { function } [ cb ] - callback when sent
 * @returns 
 */
function sendpk( sn, dstport, server, pt = 0, ssrc, payload, snoffset=0, cb ) {
  if( !ssrc ) ssrc = 25
  const ts = sn * 160
  const sendtime = sn * 20
  const uint8pl = new Uint8Array( payload.slice( sn , sn + 160 ) )
  return setTimeout( () => {
    const subheader = Buffer.alloc( 10 )
    subheader.writeUInt16BE( ( sn + snoffset ) % ( 2**16 ) )
    subheader.writeUInt32BE( ts, 2 )
    subheader.writeUInt32BE( ssrc, 6 )
    const rtppacket = Buffer.concat( [
      Buffer.from( [ 0x80, pt ] ),
      subheader,
      uint8pl ] )
    server.send( rtppacket, dstport, "localhost" )
    if( cb ) cb( { rtppacket, dstport } )
  }, sendtime )
}
/**
 * Limitation of not parsing ccrc.
 * @param { Buffer } packet
 * @return { object }
 */
function parsepk( packet ) {
  return {
    sn: packet.readUInt16BE( 2 ),
    ts: packet.readUInt32BE( 4 ),
    pt: packet.readUInt8( 1 ) & 0x7f,
    ssrc: packet.readUInt32BE( 8 ),
    payload: new Uint8Array( packet.slice( 12 ) )
  }
}
/**
 * @callback encodefunction
 * @param { Array< number > } inarray
 * @returns { Array< number > }
 */
/**
 * @callback decodefunction
 * @param { Array< number > } inarray
 * @returns { Int16Array }
 */
/**
 * Run a loop test: generate signal - endcode pass to a channel, mix with second channal
 * receive this rtp and loop back and finally recieve and test for signal in sound.
 * This tests the full audio loop with codec conversion.
 * The encode and decode functions must match the bcodec, i.e. bcodec tells 
 * projectrtp what codec to accept on that channel, the functions are what takes
 * our linear16 and encodes and decodes into the payload.
 * @param { number } acodec 
 * @param { number } bcodec 
 * @param { encodefunction } encode 
 * @param { decodefunction } decode
 * @param { number } [ ilbcpt = -1 ] if acodec is ilbc then set the dynamic pt
 */
async function looptest( acodec, bcodec, encode, decode, ilbcpt = -1 ) {
  const a = dgram.createSocket( "udp4" )
  const b = dgram.createSocket( "udp4" )
  a.bind()
  await new Promise( resolve => a.on( "listening", resolve ) )
  b.bind()
  await new Promise( resolve => b.on( "listening", resolve ) )
  let done
  const finished = new Promise( ( r ) => { done = r } )
  const channeladef = { "id": "4", "remote": { "address": "localhost", "port": a.address().port, "codec": acodec } }
  if( 97 == acodec && -1 != ilbcpt ) channeladef.remote.ilbcpt = ilbcpt
  const achannel = await projectrtp.openchannel( channeladef, function( d ) {
    if( "close" === d.action ) {
      a.close()
      b.close()
      bchannel.close()
    }
  } )
  const bchannel = await projectrtp.openchannel( { "id": "4", "remote": { "address": "localhost", "port": b.address().port, "codec": bcodec } }, function( d ) {
    if( "close" === d.action ) done()
  } )
  bchannel.mix( achannel )
  /* echo straight back */
  a.on( "message", function( msg ) {
    const rtppk = parsepk( msg )
    if( -1 != ilbcpt ) expect( rtppk.pt ).to.equal( ilbcpt )
    a.send( msg, achannel.local.port, "localhost" )
  } )
  let received = Buffer.alloc( 0 )
  let ondonereceiving, recvcount = 0
  const receiveuntil = new Promise( resolve => ondonereceiving = resolve )
  b.on( "message", function( msg ) {
    const pk = parsepk( msg )
    received = Buffer.concat( [ received, pk.payload ] )
    if( 50 < recvcount++ ) ondonereceiving()
  } )
  const y = gensignal( frequency )
  const encoded = encode( Array.from( y ) )
  for( let i = 0; 60 > i; i ++ ) {
    sendpk( i, bchannel.local.port, b, bcodec, 44, encoded )
  }
  await receiveuntil
  const y2 = decode( Array.from( received ) )
  achannel.close()
  await finished
  npl.plot( [ {
    y: Array.from( y ),
    type: "scatter"
  } ] )
  npl.plot( [ {
    y: Array.from( y2 ),
    type: "scatter"
  } ] )
  const amps = ampbyfrequency( y2 )
  expect( has( amps, frequency - 100, 25000000 ) ).to.be.false
  expect( has( amps, frequency, 25000000 ) ).to.be.true
  expect( has( amps, frequency + 100, 25000000 ) ).to.be.false
}
/**
 * Test to check we receive all packets. ALso check basic codecs to make sure
 * no duff packets come through (i,.e. memory is cleared out). It will only work
 * with non-lossy CODECS
 * @param { number } acodec 
 * @param { number } bcodec 
 * @param { encodefunction } encode 
 * @param { decodefunction } decode
 * @param { number } [ expectedval ] what value we expect after the round trip (i.e. ulaw - alawy and back again might not be the same value)
 */
async function loopcounttest( acodec, bcodec, encode, decode, expectedval = 0 ) {
  const a = dgram.createSocket( "udp4" )
  const b = dgram.createSocket( "udp4" )
  a.bind()
  await new Promise( resolve => a.on( "listening", resolve ) )
  b.bind()
  await new Promise( resolve => b.on( "listening", resolve ) )
  let done
  const finished = new Promise( ( r ) => { done = r } )
  const allstats = {
    a: {
      recv:{ count: 0 },
      send:{ count: 0 },
      port: a.address().port
    },
    b: {
      recv:{ count: 0 },
      send:{ count: 0 },
      srcport: b.address().port,
      dstport: 0
    },
    notcorrect: 0
  }
  const achannel = await projectrtp.openchannel( { "id": "4", "remote": { "address": "localhost", "port": a.address().port, "codec": acodec } }, function( d ) {
    if( "close" === d.action ) {
      a.close()
      b.close()
      bchannel.close()
      allstats.achannel = { stats: d.stats }
    }
  } )
  const bchannel = await projectrtp.openchannel( { "id": "4", "remote": { "address": "localhost", "port": b.address().port, "codec": bcodec } }, function( d ) {
    if( "close" === d.action ) {
      allstats.bchannel = { stats: d.stats }
      done()
    }
  } )
  allstats.b.dstport = bchannel.local.port
  /* echo straight back */
  a.on( "message", function( msg ) {
    a.send( msg, achannel.local.port, "localhost" )
    allstats.a.recv.count++
    allstats.a.send.count++
  } )
  bchannel.mix( achannel )
  b.on( "message", function( msg ) {
    allstats.b.recv.count++
    const pk = parsepk( msg )
    const decoded = decode( Array.from( pk.payload ) )
    for( let i = 0; i < decoded.length; i++ ) {
      if( expectedval != decoded[ i ] ) allstats.notcorrect++
    }
  } )
  const y = new Int16Array( datalength ).fill( 0 )
  const encoded = encode( Array.from( y ) )
  for( let i = 0; 60 > i; i ++ ) {
    sendpk( i, bchannel.local.port, b, bcodec, 44, encoded, 0, () => { allstats.b.send.count++ } )
  }
  const bufferdelay = 350
  const errormarin = 500
  const packettime = 20 * 60
  const totaltimerequired = packettime + bufferdelay + errormarin
  await new Promise( resolve => setTimeout( resolve, totaltimerequired ) )
  achannel.close()
  await finished
  return allstats
}
describe( "Transcode", function() {
  this.slow( 3000 )
  this.timeout( 5000 )
  it( "basic count test and data check trancode pcmu <==> pcmu", async function() {
    /* 2 seconds is important, it should be below 60 * 20mS + JT = 1200 + 300 + 300 = 1800mS - we are taking 1820 */
    this.timeout( 4000 )
    this.slow( 2000 )
    const all = []
    for( let i = 0; 50 > i; i++) {
      all.push( loopcounttest( 0, 0, lineartopcmu, pcmutolinear ) )
    }
    const results = await Promise.all( all )
    results.forEach( ( i ) => {
      expect( i.a.recv.count ).to.equal( 60 )
      expect( i.b.recv.count ).to.equal( 60 )
    } )
  } )
  it( "basic count test and data check trancode pcma <==> pcma", async function() {
    this.timeout( 3000 )
    this.slow( 2500 )
    const result = await loopcounttest( 0, 0, lineartopcma, pcmatolinear, 8 )
    expect( result.notcorrect ).to.equal( 0 )
  } )
  it( "basic count test and data check trancode pcmu <==> pcma", async function() {
    this.timeout( 3000 )
    this.slow( 2500 )
    const result = await loopcounttest( 8, 0, lineartopcmu, pcmutolinear, 8 )
    expect( result.notcorrect ).to.equal( 0 )
  } )
  it( "basic count test and data check trancode pcma <==> pcmu", async function() {
    this.timeout( 3000 )
    this.slow( 2500 )
    const result = await loopcounttest( 0, 8, lineartopcma, pcmatolinear, 8 )
    expect( result.notcorrect ).to.equal( 0 )
  } )
  it( "Test our linear to pcma converting routines", async function() {
    const y = gensignal( frequency )
    const pcma = lineartopcma( Array.from( y ) )
    const y2 = pcmatolinear( pcma )
    npl.plot( [ {
      y: y2,
      type: "scatter"
    } ] )
    const amps = ampbyfrequency( y )
    expect( has( amps, 300, 25000000 ) ).to.be.false
    expect( has( amps, 400, 25000000 ) ).to.be.true
    expect( has( amps, 500, 25000000 ) ).to.be.false
  } )
  it( "trancode pcmu <==> ilbc static pt", async function() {
    await looptest( 97, 0, lineartopcmu, pcmutolinear )
  } )
  it( "trancode pcmu <==> ilbc with dynamic pt", async function() {
    await looptest( 97, 0, lineartopcmu, pcmutolinear, 123 )
  } )
  it( "trancode pcmu <==> g722", async function() {
    await looptest( 9, 0, lineartopcmu, pcmutolinear )
  } )
  it( "trancode pcmu <==> pcma", async function() {
    await looptest( 8, 0, lineartopcmu, pcmutolinear )
  } )
  it( "trancode pcma <==> ilbc", async function() {
    await looptest( 97, 8, lineartopcma, pcmatolinear )
  } )
  it( "trancode pcma <==> g722", async function() {
    await looptest( 9, 8, lineartopcma, pcmatolinear )
  } )
  it( "trancode pcma <==> pcmu", async function() {
    await looptest( 0, 8, lineartopcma, pcmatolinear )
  } )
  it( "trancode pcma <==> pcma", async function() {
    await looptest( 8, 8, lineartopcma, pcmatolinear )
  } )
  it( "trancode pcmu <==> pcmu", async function() {
    await looptest( 0, 0, lineartopcmu, pcmutolinear )
  } )
  it( "simulate an xfer with multiple mix then test new path pcma <==> g722", async function() {
    this.timeout( 8000 )
    this.slow( 7000 )
    /* make sure we have some tone to play */
    projectrtp.tone.generate( "300*0.5:2000", "/tmp/tone.wav" )
    projectrtp.tone.generate( "800*0.5:2000", "/tmp/hightone.wav" )
    /**
     * a and b is the 2 phone legs, c is the transfered channel
     */
    const acodec = 8
    const bcodec = 0
    const ccodec = 9
    /*
    a = 8, b = 0, c = 0 missing tones on a
    a = 8, b = 9, c = 0 all works
    a = 8, b = 0, c = 9, missing tones and missing c leg back on a
    */
    const a = dgram.createSocket( "udp4" )
    const b = dgram.createSocket( "udp4" )
    const c = dgram.createSocket( "udp4" )
    a.bind()
    await new Promise( resolve => a.on( "listening", resolve ) )
    b.bind()
    await new Promise( resolve => b.on( "listening", resolve ) )
    c.bind()
    await new Promise( resolve => c.on( "listening", resolve ) )
    /* echo straight back */
    c.on( "message", function( msg ) {
      c.send( msg, cchannel.local.port, "localhost" )
    } )
    b.on( "message", function( msg ) {
      b.send( msg, bchannel.local.port, "localhost" )
    } )
    let received = Buffer.alloc( 0 )
    let ondonereceiving, recvcount = 0
    const receiveuntil = new Promise( resolve => ondonereceiving = resolve )
    a.on( "message", function( msg ) {
      const pk = parsepk( msg )
      received = Buffer.concat( [ received, pk.payload ] )
      if( 100 < recvcount++ ) ondonereceiving()
    } )
    let done
    const finished = new Promise( ( resolve ) => { done = resolve } )
    let unmixresolve
    const unmixdone = new Promise( resolve => unmixresolve = resolve )
    /* This channel reflects the outbound channel */
    const achannel = await projectrtp.openchannel( { "id": "4" }, function( d ) {
      if( "close" === d.action ) {
        a.close()
        b.close()
        c.close()
        cchannel.close()
      }
      if( "mix" === d.action && "finished" === d.event ) unmixresolve()
    } )
    await new Promise( resolve => setTimeout( resolve, 800 ) )
    achannel.remote( { "address": "localhost", "port": a.address().port, "codec": acodec } )
    /* This channel reflects the originator */
    const bchannel = await projectrtp.openchannel( { "id": "5", "remote": { "address": "localhost", "port": b.address().port, "codec": bcodec } }, function( /*d*/ ) {
    } )
    achannel.mix( bchannel )
    await new Promise( resolve => setTimeout( resolve, 500 ) ) // we really should wait for the mix start events
    /* for some reason in our lib this gets sent again */
    achannel.remote( { "address": "localhost", "port": a.address().port, "codec": acodec } )
    bchannel.remote( { "address": "localhost", "port": b.address().port, "codec": bcodec } )
    achannel.mix( bchannel )
    bchannel.record( { file: "/tmp/test.wav", numchannels: 2, mp3: true } )
    const y = gensignal( 100 )
    const encoded = lineartopcma( Array.from( y ) )
    for( let i = 0 ; 120 > i; i ++ ) {
      sendpk( i, achannel.local.port, a, acodec, 44, encoded, 6300 )
    }
    await new Promise( resolve => setTimeout( resolve, 1200 ) )
    /* Now our blind xfer happens */
    achannel.unmix()
    bchannel.unmix()
    await unmixdone
    
    /* moh followed by ringing tone */
    achannel.play( { "loop": true, "files": [ { "wav": "/tmp/tone.wav" } ] } )
    await new Promise( resolve => setTimeout( resolve, 200 ) )
    achannel.play( { "loop": true, "files": [ { "wav": "/tmp/hightone.wav" } ] } )
    await new Promise( resolve => setTimeout( resolve, 200 ) )
    /* now open our new leg */
    const cchannel = await projectrtp.openchannel( { "id": "6" }, function( d ) {
      if( "close" === d.action ) done()
    } )
    bchannel.close()
    cchannel.remote( { "address": "localhost", "port": c.address().port, "codec": ccodec } )
    await new Promise( resolve => setTimeout( resolve, 40 ) )
    cchannel.mix( achannel )
    /* now we have one way audio in real life */
    await receiveuntil
    const y2 = pcmatolinear( Array.from( received ) )
    await new Promise( resolve => setTimeout( resolve, 1000 ) )
    achannel.close()
    await finished
    npl.plot( [ {
      y: Array.from( y ),
      type: "scatter"
    } ] )
    npl.plot( [ {
      y: Array.from( y2 ),
      type: "scatter"
    } ] )
    /* TODO this currently doesn't test the c leg as this is teh same frequency as the a leg*/
    const amps = ampbyfrequency( y2 )
    expect( has( amps, 100, 25000000 ) ).to.be.true
    expect( has( amps, 300, 25000000 ) ).to.be.true
    expect( has( amps, 800, 25000000 ) ).to.be.true
    expect( has( amps, 500, 25000000 ) ).to.be.false
    await fs.promises.unlink( "/tmp/ukringing.wav" ).catch( () => {} )
  } )
  it( "replay captured g722 from poly", async () => {
    const g722endpoint = dgram.createSocket( "udp4" )
    g722endpoint.on( "message", function() {} )
    const pcmuendpoint = dgram.createSocket( "udp4" )
    let receivedpcmu = []
    pcmuendpoint.on( "message", function( msg ) {
      pcmuendpoint.send( msg, pcmuchannel.local.port, "localhost" )
      receivedpcmu = [ ...receivedpcmu,  ...Array.from( pcmutolinear( parsepk( msg ).payload ) ) ]
    } )
    g722endpoint.bind()
    await new Promise( resolve => g722endpoint.on( "listening", resolve ) )
    pcmuendpoint.bind()
    await new Promise( resolve => pcmuendpoint.on( "listening", resolve ) )
    const allstats = {}
    const g722channel = await projectrtp.openchannel( { "id": "4", "remote": { "address": "localhost", "port": g722endpoint.address().port, "codec": 9 } }, function( d ) {
      if( "close" === d.action ) {
        g722endpoint.close()
        pcmuendpoint.close()
        pcmuchannel.close()
        allstats.achannel = { stats: d.stats }
      }
    } )
    let done
    const allclose = new Promise( resolve => done = resolve )
    const pcmuchannel = await projectrtp.openchannel( { "id": "4", "remote": { "address": "localhost", "port": pcmuendpoint.address().port, "codec": 0 } }, function( d ) {
      if( "close" === d.action ) {
        allstats.bchannel = { stats: d.stats }
        done()
      }
    } )
    const ourpcap = ( await pcap.readpcap( "test/interface/pcaps/440hzinbackgroundg722.pcap" ) ).slice( 0, 50 )
    g722channel.mix( pcmuchannel )
    const offset = 0
    ourpcap.forEach( ( packet ) => {
      if( packet.ipv4 && packet.ipv4.udp && 10018 == packet.ipv4.udp.dstport ) {
        sendpayload( ( 1000 * packet.ts_sec_offset ) - offset, packet.ipv4.udp.data, g722channel.local.port, g722endpoint )
      }
    } )
    await new Promise( resolve => setTimeout( resolve, 1400 ) )
    g722channel.close()
    await allclose
    npl.plot( [ {
      y: Array.from( receivedpcmu ),
      type: "scatter"
    } ] )
    const amps = ampbyfrequency( Int16Array.from( receivedpcmu ) )
    const bin = 225
    expect( 20000 < amps[ bin ] ).to.be.true
    npl.plot( [ {
      y: Array.from( amps ),
      type: "scatter"
    } ] )
  } )
  it( "replay captured g722 no transcode from poly 3 way mix", async () => {
    const g722endpoint = dgram.createSocket( "udp4" )
    g722endpoint.on( "message", function() {} )
    const pcmuendpoint = dgram.createSocket( "udp4" )
    let receivedpcmu = []
    pcmuendpoint.on( "message", function( msg ) {
      pcmuendpoint.send( msg, pcmuchannel.local.port, "localhost" )
      receivedpcmu = [ ...receivedpcmu,  ...Array.from( pcmutolinear( parsepk( msg ).payload ) ) ]
    } )
    g722endpoint.bind()
    await new Promise( resolve => g722endpoint.on( "listening", resolve ) )
    pcmuendpoint.bind()
    await new Promise( resolve => pcmuendpoint.on( "listening", resolve ) )
    const allstats = {}
    const g722channel = await projectrtp.openchannel( { "id": "4", "remote": { "address": "localhost", "port": g722endpoint.address().port, "codec": 9 } }, function( d ) {
      if( "close" === d.action ) {
        g722endpoint.close()
        pcmuendpoint.close()
        pcmuchannel.close()
        secondg722.close()
        allstats.achannel = { stats: d.stats }
      }
    } )
    let done
    const allclose = new Promise( resolve => done = resolve )
    const pcmuchannel = await projectrtp.openchannel( { "id": "4", "remote": { "address": "localhost", "port": pcmuendpoint.address().port, "codec": 0 } }, function( d ) {
      if( "close" === d.action ) {
        allstats.bchannel = { stats: d.stats }
        done()
      }
    } )
    const secondg722 = await projectrtp.openchannel( { "id": "4", "remote": { "address": "localhost", "port": 9990, "codec": 9 } }, function( d ) {
      if( "close" === d.action ) {
        allstats.bchannel = { stats: d.stats }
        done()
      }
    } )
    const ourpcap = ( await pcap.readpcap( "test/interface/pcaps/440hzinbackgroundg722.pcap" ) ).slice( 0, 50 )
    g722channel.mix( pcmuchannel )
    g722channel.mix( secondg722 )
    const offset = 0
    ourpcap.forEach( ( packet ) => {
      if( packet.ipv4 && packet.ipv4.udp && 10018 == packet.ipv4.udp.dstport ) {
        sendpayload( ( 1000 * packet.ts_sec_offset ) - offset, packet.ipv4.udp.data, g722channel.local.port, g722endpoint )
      }
    } )
    await new Promise( resolve => setTimeout( resolve, 1400 ) )
    g722channel.close()
    await allclose
    npl.plot( [ {
      y: Array.from( receivedpcmu ),
      type: "scatter"
    } ] )
    const amps = ampbyfrequency( Int16Array.from( receivedpcmu ) )
    npl.plot( [ {
      y: Array.from( amps ),
      type: "scatter"
    } ] )
    const bin = 430
    expect( 20000 < amps[ bin ] ).to.be.true
  } )
} )