Skip to content

Commit ea2dbd3

Browse files
committed
Imported RTP changes 2 from userconcept.
1 parent 09863af commit ea2dbd3

8 files changed

Lines changed: 294 additions & 85 deletions

File tree

Libraries/ecma-rtp/codec.mjs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,83 @@
1515
* along with ecma-rtp. If not, see <https://www.gnu.org/licenses/>.
1616
*/
1717

18+
19+
import alawmulaw from 'alawmulaw';
20+
import waveResampler from 'wave-resampler';
21+
22+
/**
23+
* Superclass to represent an audio Codec.
24+
*/
25+
class Codec {
26+
constructor ({clientRate = 16000,
27+
clientDepth = 16,
28+
serverRate = 8000,
29+
serverDepth = 8} = {}) {
30+
31+
this.clientRate = clientRate;
32+
this.clientDepth = clientDepth;
33+
this.serverRate = serverRate;
34+
this.serverDepth = serverDepth;
35+
36+
this.samples = []
37+
}
38+
39+
transcode () {
40+
41+
}
42+
43+
/**
44+
* We need to do some testing to see how resampling options affect STT because
45+
*
46+
* Cubic without Low Pass Filter:
47+
* generate: 0.283ms
48+
* transcode: 0.144ms
49+
* resample: 0.442ms
50+
*
51+
* Cubic with LPF:
52+
* generate: 0.375ms
53+
* transcode: 0.123ms
54+
* resample: 6.605ms
55+
*
56+
* That's on 20ms audio on an i5 9500. Interestingly it scales x2 per x10 duration,
57+
* so it's not terrible - but if we can shave time off, so much the better
58+
*
59+
* First test - LPF Off doesn't seem to hurt anything!
60+
*/
61+
static resampleAudio (samples) {
62+
var wavSamples = alawmulaw.mulaw.decode(samples);
63+
var floatSamples=Float32Array.from(Float32Array.from(wavSamples).map(x=>x/0x8000));
64+
var newSamples = waveResampler.resample(floatSamples, 8000, 16000, {method: "cubic", LPF: false}); // RETURNS A FLOAT64 NOT AN INT16 READ THE DOCS
65+
var samples1616=Int16Array.from(newSamples.map(x => (x>0 ? x*0x7FFF : x*0x8000)));
66+
return Buffer.from(samples1616.buffer);
67+
}
68+
69+
set samples (samples) {
70+
this.samples.push(this.transcode(samples));
71+
}
72+
73+
get samples () {
74+
return this.samples.pop;
75+
}
76+
}
77+
78+
/**
79+
* Represents mulaw (as used by Combadges)
80+
*/
81+
class muLaw8K extends Codec {
82+
constructor ({} = {}) {
83+
super({clientRate: 8000,
84+
clientDepth: 16,
85+
serverRate: 16000,
86+
serverDepth: 8});
87+
}
88+
89+
decode (samples, callback) {
90+
wavSamples = samples;
91+
//var wavSamples = alawmulaw.mulaw.decode(samples);
92+
var reSamples = Codec.resample(wavSamples);
93+
callback(reSamples);
94+
}
95+
}
96+
97+
export { Codec }

Libraries/ecma-rtp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "ecma-rtp",
2+
"name": "@combadge/ecma-rtp",
33
"version": "0.0.5",
44
"type": "module",
55
"description": "An RTP Library for Spin Doctor, written in ECMAScript",

Libraries/ecma-rtp/rtp.mjs

Lines changed: 88 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
*/
2323

2424

25-
import alawmulaw from 'alawmulaw'; // For now, move to codec.mjs later.
25+
import dgram from 'dgram';
26+
import { Codec } from './codec.mjs';
2627

2728
const Payload = {
2829
Mulaw8K: 0,
@@ -136,10 +137,9 @@ class RTPHeader {
136137

137138
// var payloadType = Payload[findPayload(payloadType)] // Brutish validation to check supported Payload type. Not sure we're there yet.
138139
var payloadSize = (packet.slice(lengthOfHeader).length);
139-
140-
var sequenceNo = parseInt(packet.slice(2,4));
141-
var timeStamp = parseInt(packet.slice(4,8));
142-
var ssrc = parseInt(packet.slice(8,12));
140+
var sequenceNo = packet.slice(2,4).readUInt16BE();
141+
var timeStamp = packet.slice(4,8).readUInt16BE();
142+
var ssrc = packet.slice(8,12).readUInt16BE();
143143

144144
var header = new RTPHeader(payloadSize, payloadType, ssrc);
145145
header.forceHeader(sequenceNo, timeStamp);
@@ -217,13 +217,7 @@ class RTPHeader {
217217
class RTPPacket {
218218
constructor(header, payload = undefined) {
219219
this.header = header;
220-
221-
if (this.header.payloadType == Payload.Mulaw8K) {
222-
this._payload = alawmulaw.mulaw.decode(payload); // For now, brute force the transcode, since we know we're working in mulaw.
223-
} else {
224-
console.log(this.header);
225-
throw "Not Mulaw, unsure what to do!";
226-
}
220+
this._payload = payload;
227221
}
228222

229223
/**
@@ -275,14 +269,92 @@ class RTPHeader {
275269
}
276270

277271
/**
278-
* Temporary class - replace with RTPPacket instances when you work out how.
272+
* Should provide a source and sink for RTP Packets. For now, hardcode to the correct values for Combadge audio.
273+
*
274+
* Down the line, we should generalise this to allow the library to be reused elsewhere -
275+
* (since I wouldn't be writing it if there was another approachable JS RTP library)
279276
*/
280-
class RTPStream {
281-
constructor() {
277+
class RTPServer {
278+
constructor(listenAddress, portNumber, consumer = undefined) {
282279
this.sampleCount = new Number(160); // At 8 bits/sample, this can be used for both incrementing the timestamp AND counting bytes.
283280
this.header = new RTPHeader(this.sampleCount);
281+
this._consumer = undefined;
282+
283+
if (portNumber % 2 != 0) {
284+
throw `RTP Ports must be even. Odd-numbered ports are reserved for RTCP. Invalid port ${portNumber} passed.`;
285+
}
286+
287+
if (consumer) {
288+
this._consumer = consumer;
289+
}
290+
291+
this.udpServer = dgram.createSocket('udp4');
292+
this.udpServer.bind(portNumber, listenAddress)
293+
this.udpServer.on('listening', () => {
294+
const address = this.udpServer.address();
295+
console.log(`RTP Server spawned at ${address.address}:${address.port}`);
296+
})
297+
this.udpServer.on('message', (message, clientInfo) => {
298+
var packet = RTPPacket.from(message);
299+
this.receivePacket(packet);
300+
})
301+
302+
/*
303+
this.transcoding = true;
304+
var floatSamples=Float32Array.from(Float32Array.from(this.recSamples).map(x=>x/0x8000));
305+
var newSamples = waveResampler.resample(floatSamples, 8000, 16000); // RETURNS A FLOAT64 NOT AN INT16 READ THE DOCS
306+
var samples1616=Int16Array.from(newSamples.map(x => (x>0 ? x*0x7FFF : x*0x8000)));
307+
var wav16buffer = new Buffer.from(samples1616.buffer);
308+
console.log("Result:", model.stt(wav16buffer));
309+
this.transcoding = false;
310+
*/
284311
}
285312

313+
/**
314+
* Function to pass in a function to receive audio from the server.
315+
*/
316+
set consumer (consumer) {
317+
this._consumer = consumer;
318+
}
319+
320+
get consumer () {
321+
return this._consumer;
322+
}
323+
324+
/** Do something with the packet, then forward it to the Consumer if set, or cache it if not. */
325+
receivePacket (packet) {
326+
var samples = Codec.resampleAudio(packet.payload);
327+
this.consumer.receiveSamples(samples);
328+
}
329+
330+
/**
331+
* Queue audio to be sent to a remote UDP target.
332+
*/
333+
queueAudio (audio) {
334+
335+
}
336+
337+
/**
338+
* Register a recipient to receive audio that's passed to queueAudio - basically, a remote target for an RTP stream.
339+
* Address should be one of my fancy IP address objects, which I need to move outside the cccp protocol I think.
340+
*
341+
* @param {*} address
342+
*/
343+
registerRemote (address) {
344+
345+
}
346+
347+
348+
349+
/**
350+
* Send audio to a remote device. We'll be explicit about target, rather than just using send() from the responder
351+
* because otherwise I can already see an exploit where a snooper sends 0-length packets to ensure they're always the most recent address.
352+
* socket.send(msg[, offset, length][, port][, address][, callback])
353+
*/
354+
sendPacket (packet, address, port) {
355+
packetData = packet.toBuffer()
356+
this.udpServer.send(packetData, 0, packetData.length, port, address)
357+
}
286358

287359
/**
288360
* Return a completed media packet without transcoding.
@@ -308,25 +380,4 @@ class RTPStream {
308380
}
309381
}
310382

311-
class MediaQueue {
312-
constructor() {
313-
this.inQueue = new Array();
314-
this.outQueue = new Array();
315-
}
316-
317-
/**
318-
* Add a packet to the queue for RTP decoding
319-
*/
320-
addReceivedPacket (packet, callback) {
321-
return null;
322-
}
323-
324-
/**
325-
* Add a media stream for RTP encoding
326-
*/
327-
addTransmitMedia (samples, callback) {
328-
return null;
329-
}
330-
}
331-
332-
export { Payload, RTPHeader, RTPStream, RTPPacket, MediaQueue };
383+
export { Payload, RTPHeader, RTPPacket, RTPServer };
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* Robin-agent - an interactive voice agent for Spin Doctor
3+
* Copyright (C) 2021-2022 The Combadge Project by mo-g
4+
*
5+
* Robin-agent is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, version 3 of the License.
8+
*
9+
* Robin-agent is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with Robin-agent. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
/**
19+
* tone({
20+
freq: Tones["E♭6"],
21+
lengthInSecs: 0.2,
22+
volume: tone.MAX_16/2,
23+
rate: 16000,
24+
shape: 'sine'
25+
})
26+
*/
27+
28+
29+
import { Tones, FixedPitch, Silence} from '@combadge/ecma-tonegenerator'
30+
31+
32+
/**
33+
* Superclass for all nonverbal sound generators. By default, we should generate audio in the same codec and profile as Robin uses internally.
34+
*
35+
*
36+
*/
37+
class Sound {
38+
constructor (notes) {
39+
this.tones = notes.map(function (note) {
40+
var tone = new FixedPitch({frequency:Tones[note],
41+
bitDepth: 16,
42+
sampleRate: 16000});
43+
var silence = new Silence({bitDepth: 16,
44+
sampleRate: 16000});
45+
46+
return [tone.approximate({duration:500}), silence.accurate({duration:500})];
47+
});
48+
49+
}
50+
}
51+
52+
/**
53+
* Click, B5, Click, B5, Click, D6
54+
*/
55+
class ListeningChirp extends Sound {
56+
constructor () {
57+
notes = ["B5", "B5", "D6"];
58+
super(notes);
59+
}
60+
61+
get output () {
62+
output = [];
63+
this.tones.forEach(function (tone) {
64+
tone.forEach(function (sample) {
65+
output.push(sample);
66+
})
67+
});
68+
return new Int16Array(output);
69+
}
70+
}
71+
72+
/**
73+
*
74+
*/
75+
class OfflineChirp extends Sound {
76+
constructor () {
77+
notes = ["B5", "B5"];
78+
super(notes);
79+
}
80+
}
81+
82+
/**
83+
* B5, D6, B5
84+
*/
85+
class BoatswainCall extends Sound {
86+
constructor () {
87+
notes = ["B5", "D6", "B5"];
88+
super(notes);
89+
}
90+
}
91+
92+
export { ListeningChirp, OfflineChirp, BotswainWhistle };

Libraries/robin-agent/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "robin-agent",
2+
"name": "@combadge/robin-agent",
33
"version": "0.0.1",
44
"type": "module",
55
"description": "An interactive voice agent for Spin Doctor.",

0 commit comments

Comments
 (0)