sip-lab 1.21.0 → 1.23.0

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,195 @@
1
+ var sip = require ('../index.js')
2
+ var Zeq = require('@mayama/zeq')
3
+ var z = new Zeq()
4
+ var m = require('data-matching')
5
+ var sip_msg = require('sip-matching')
6
+ var sdp = require('sdp-matching')
7
+
8
+ async function test() {
9
+ //sip.set_log_level(6)
10
+ sip.dtmf_aggregation_on(500)
11
+
12
+ z.trap_events(sip.event_source, 'event', (evt) => {
13
+ var e = evt.args[0]
14
+ return e
15
+ })
16
+
17
+ console.log(sip.start((data) => { console.log(data)} ))
18
+
19
+ t1 = sip.transport.create({address: "127.0.0.1", type: 'tcp'})
20
+ t2 = sip.transport.create({address: "127.0.0.1", type: 'tcp'})
21
+
22
+ console.log("t1", t1)
23
+ console.log("t2", t2)
24
+
25
+ oc = sip.call.create(t1.id, {
26
+ from_uri: '"abc"<sip:alice@test.com>',
27
+ to_uri: `sip:bob@${t2.address}:${t2.port}`,
28
+ headers: {
29
+ 'X-MyHeader1': 'abc',
30
+ 'X-MyHeader2': 'def',
31
+ },
32
+ })
33
+
34
+ await z.wait([
35
+ {
36
+ event: "incoming_call",
37
+ call_id: m.collect("call_id"),
38
+ msg: sip_msg({
39
+ $rm: 'INVITE',
40
+ $fU: 'alice',
41
+ $fd: 'test.com',
42
+ $tU: 'bob',
43
+ '$hdr(X-MyHeader1)': 'abc',
44
+ 'hdr_x_myheader2': 'def',
45
+ }),
46
+ },
47
+ {
48
+ event: 'response',
49
+ call_id: oc.id,
50
+ method: 'INVITE',
51
+ msg: sip_msg({
52
+ $rs: '100',
53
+ $rr: 'Trying',
54
+ '$(hdrcnt(via))': 1,
55
+ 'hdr_call_id': m.collect('sip_call_id'),
56
+ $fU: 'alice',
57
+ $fd: 'test.com',
58
+ $tU: 'bob',
59
+ '$hdr(l)': '0',
60
+ }),
61
+ },
62
+ ], 1000)
63
+
64
+ ic = {
65
+ id: z.store.call_id,
66
+ sip_call_id: z.store.sip_call_id,
67
+ }
68
+
69
+ sip.call.respond(ic.id, {
70
+ code: 200,
71
+ reason:'OK',
72
+ headers: {
73
+ 'X-MyHeader3': 'ghi',
74
+ 'X-MyHeader4': 'jkl',
75
+ },
76
+ })
77
+
78
+ await z.wait([
79
+ {
80
+ event: 'media_update',
81
+ call_id: oc.id,
82
+ status: 'ok',
83
+ },
84
+ {
85
+ event: 'media_update',
86
+ call_id: ic.id,
87
+ status: 'ok',
88
+ },
89
+ {
90
+ event: 'response',
91
+ call_id: oc.id,
92
+ method: 'INVITE',
93
+ msg: sip_msg({
94
+ $rs: '200',
95
+ $rr: 'OK',
96
+ '$(hdrcnt(v))': 1,
97
+ $fU: 'alice',
98
+ $fd: 'test.com',
99
+ $tU: 'bob',
100
+ '$hdr(content-type)': 'application/sdp',
101
+ $rb: '!{_}a=sendrecv',
102
+ '$hdr(X-MyHeader3)': 'ghi',
103
+ '$hdr(X-MyHeader4)': 'jkl',
104
+ }),
105
+ },
106
+ ], 1000)
107
+
108
+ await z.sleep(500)
109
+
110
+ sip.call.start_record_wav(oc.id, {file: './oc.wav'})
111
+ sip.call.start_record_wav(ic.id, {file: './ic.wav'})
112
+
113
+ sip.call.send_dtmf(oc.id, {digits: '1234', mode: 1})
114
+ sip.call.send_dtmf(ic.id, {digits: '1234', mode: 1})
115
+
116
+ await z.wait([
117
+ {
118
+ event: 'dtmf',
119
+ call_id: ic.id,
120
+ digits: '1234',
121
+ mode: 1,
122
+ media_id: 0
123
+ },
124
+ {
125
+ event: 'dtmf',
126
+ call_id: oc.id,
127
+ digits: '1234',
128
+ mode: 1,
129
+ media_id: 0
130
+ },
131
+ ], 3000)
132
+
133
+ sip.call.start_speech_synth(oc.id, {voice: 'slt', text: 'Hello World.', end_of_speech_event: true})
134
+ sip.call.start_speech_synth(ic.id, {voice: 'kal', text: 'How are you?', end_of_speech_event: true, no_loop: true})
135
+
136
+ await z.wait([
137
+ {
138
+ event: 'end_of_speech',
139
+ call_id: ic.id,
140
+ },
141
+ {
142
+ event: 'end_of_speech',
143
+ call_id: oc.id,
144
+ },
145
+ ], 2000)
146
+
147
+ await z.wait([
148
+ {
149
+ event: 'end_of_speech',
150
+ call_id: oc.id,
151
+ },
152
+ ], 2000)
153
+
154
+ sip.call.stop_speech_synth(oc.id) // this is not actually necessary. It is used just to confirm the command works
155
+ sip.call.stop_speech_synth(ic.id) // this is not actually necessary. It is used just to confirm the command works
156
+
157
+ sip.call.stop_record_wav(oc.id)
158
+ sip.call.stop_record_wav(ic.id)
159
+
160
+ sip.call.terminate(oc.id)
161
+
162
+ await z.wait([
163
+ {
164
+ event: 'call_ended',
165
+ call_id: oc.id,
166
+ },
167
+ {
168
+ event: 'call_ended',
169
+ call_id: ic.id,
170
+ },
171
+ {
172
+ event: 'response',
173
+ call_id: oc.id,
174
+ method: 'BYE',
175
+ msg: sip_msg({
176
+ $rs: '200',
177
+ $rr: 'OK',
178
+ }),
179
+ },
180
+ ], 1000)
181
+
182
+ await z.sleep(1000)
183
+
184
+ console.log("Success")
185
+
186
+ sip.stop()
187
+ }
188
+
189
+
190
+ test()
191
+ .catch(e => {
192
+ console.error(e)
193
+ process.exit(1)
194
+ })
195
+
package/src/addon.cpp CHANGED
@@ -569,6 +569,40 @@ Napi::Value call_start_fax(const Napi::CallbackInfo &info) {
569
569
  return env.Null();
570
570
  }
571
571
 
572
+ Napi::Value call_start_speech_synth(const Napi::CallbackInfo &info) {
573
+ Napi::Env env = info.Env();
574
+
575
+ if (info.Length() != 2) {
576
+ Napi::Error::New(env,
577
+ "Wrong number of arguments. Expected: call_id, params.")
578
+ .ThrowAsJavaScriptException();
579
+ return env.Null();
580
+ }
581
+
582
+ if (!info[0].IsNumber()) {
583
+ Napi::TypeError::New(env, "call_id must be number.")
584
+ .ThrowAsJavaScriptException();
585
+ return env.Null();
586
+ }
587
+ int call_id = info[0].As<Napi::Number>().Int32Value();
588
+
589
+ if (!info[1].IsString()) {
590
+ Napi::TypeError::New(env, "params must be a JSON string.")
591
+ .ThrowAsJavaScriptException();
592
+ return env.Null();
593
+ }
594
+ const string json = info[1].As<Napi::String>().Utf8Value();
595
+
596
+ int res = pjw_call_start_speech_synth(call_id, json.c_str());
597
+
598
+ if (res != 0) {
599
+ Napi::Error::New(env, pjw_get_error()).ThrowAsJavaScriptException();
600
+ return env.Null();
601
+ }
602
+
603
+ return env.Null();
604
+ }
605
+
572
606
  Napi::Value call_stop_record_wav(const Napi::CallbackInfo &info) {
573
607
  Napi::Env env = info.Env();
574
608
 
@@ -670,6 +704,40 @@ Napi::Value call_stop_fax(const Napi::CallbackInfo &info) {
670
704
  return env.Null();
671
705
  }
672
706
 
707
+ Napi::Value call_stop_speech_synth(const Napi::CallbackInfo &info) {
708
+ Napi::Env env = info.Env();
709
+
710
+ if (info.Length() != 2) {
711
+ Napi::Error::New(env, "Wrong number of arguments. Expected: call_id")
712
+ .ThrowAsJavaScriptException();
713
+ return env.Null();
714
+ }
715
+
716
+ if (!info[0].IsNumber()) {
717
+ Napi::TypeError::New(env, "call_id must be number.")
718
+ .ThrowAsJavaScriptException();
719
+ return env.Null();
720
+ }
721
+ int call_id = info[0].As<Napi::Number>().Int32Value();
722
+
723
+ if (!info[1].IsString()) {
724
+ Napi::TypeError::New(env, "params must be a JSON string.")
725
+ .ThrowAsJavaScriptException();
726
+ return env.Null();
727
+ }
728
+ const string json = info[1].As<Napi::String>().Utf8Value();
729
+
730
+ int res = pjw_call_stop_speech_synth(call_id, json.c_str());
731
+
732
+ if (res != 0) {
733
+ Napi::Error::New(env, pjw_get_error()).ThrowAsJavaScriptException();
734
+ return env.Null();
735
+ }
736
+
737
+ return env.Null();
738
+ }
739
+
740
+
673
741
  Napi::Value call_get_stream_stat(const Napi::CallbackInfo &info) {
674
742
  Napi::Env env = info.Env();
675
743
 
@@ -1265,11 +1333,15 @@ Napi::Object init(Napi::Env env, Napi::Object exports) {
1265
1333
  exports.Set("call_start_play_wav",
1266
1334
  Napi::Function::New(env, call_start_play_wav));
1267
1335
  exports.Set("call_start_fax", Napi::Function::New(env, call_start_fax));
1336
+
1337
+ exports.Set("call_start_speech_synth", Napi::Function::New(env, call_start_speech_synth));
1338
+
1268
1339
  exports.Set("call_stop_record_wav",
1269
1340
  Napi::Function::New(env, call_stop_record_wav));
1270
1341
  exports.Set("call_stop_play_wav",
1271
1342
  Napi::Function::New(env, call_stop_play_wav));
1272
1343
  exports.Set("call_stop_fax", Napi::Function::New(env, call_stop_fax));
1344
+ exports.Set("call_stop_speech_synth", Napi::Function::New(env, call_stop_speech_synth));
1273
1345
  exports.Set("call_get_stream_stat",
1274
1346
  Napi::Function::New(env, call_get_stream_stat));
1275
1347
  // exports.Set("call_refer", Napi::Function::New(env, call_refer));
@@ -49,14 +49,9 @@ int make_evt_dtmf(char *dest, int size, long call_id, int digits_len,
49
49
 
50
50
  int make_evt_call_ended(char *dest, int size, long call_id, int sip_msg_len,
51
51
  const char *sip_msg) {
52
- printf("make_evt_call_ended sip_msg_len=%i sip_msg=%s\n", sip_msg_len,
52
+ printf("make_evt_call_ended sip_msg_len=%i sip_msg=%p\n", sip_msg_len,
53
53
  sip_msg);
54
- if (!sip_msg || sip_msg == (char *)0xc000000000000) {
55
- // received invalid pointer to sip_msg so do not add the message to the
56
- // event
57
- return snprintf(dest, size, "{\"event\": \"call_ended\", \"call_id\": %ld}",
58
- call_id);
59
- } else if (sip_msg_len > 500 && sip_msg_len < 2000 && sip_msg) {
54
+ if (sip_msg_len > 500 && sip_msg_len < 2000 && sip_msg) {
60
55
  /* sip_msg_len sometimes show up as a large value like sip_msg_len=11560297
61
56
  * which seems to be a bug in pjsip */
62
57
  return snprintf(dest, size,
@@ -104,6 +99,18 @@ int make_evt_fax_result(char *dest, int size, long call_id, int result) {
104
99
  result);
105
100
  }
106
101
 
102
+ int make_evt_end_of_file(char *dest, int size, long call_id) {
103
+ return snprintf(
104
+ dest, size,
105
+ "{\"event\": \"end_of_file\", \"call_id\": %ld}", call_id);
106
+ }
107
+
108
+ int make_evt_end_of_speech(char *dest, int size, long call_id) {
109
+ return snprintf(
110
+ dest, size,
111
+ "{\"event\": \"end_of_speech\", \"call_id\": %ld}", call_id);
112
+ }
113
+
107
114
  int make_evt_tcp_msg(char *dest, int size, long call_id, const char *protocol, char *data, int data_len) {
108
115
  return snprintf(
109
116
  dest, size,
@@ -34,6 +34,10 @@ int make_evt_registration_status(char *dest, int size, long account_id,
34
34
 
35
35
  int make_evt_fax_result(char *dest, int size, long call_id, int result);
36
36
 
37
+ int make_evt_end_of_file(char *dest, int size, long call_id);
38
+
39
+ int make_evt_end_of_speech(char *dest, int size, long call_id);
40
+
37
41
  int make_evt_tcp_msg(char *dest, int size, long call_id, const char *protocol, char *data, int data_len);
38
42
 
39
43
  #endif
@@ -0,0 +1,32 @@
1
+ #ifndef __FLITE_PORT_H__
2
+ #define __FLITE_PORT_H__
3
+
4
+ #include <pjmedia/port.h>
5
+
6
+ PJ_BEGIN_DECL
7
+
8
+ enum pjmedia_filte_option
9
+ {
10
+ PJMEDIA_SPEECH_NO_LOOP = 1
11
+ };
12
+
13
+ PJ_DEF(pj_status_t) pjmedia_flite_port_create( pj_pool_t *pool,
14
+ unsigned clock_rate,
15
+ unsigned channel_count,
16
+ unsigned samples_per_frame,
17
+ unsigned bits_per_sample,
18
+ const char *voice,
19
+ pjmedia_port **p_port);
20
+
21
+ PJ_DEF(pj_status_t) pjmedia_flite_port_set_eof_cb(pjmedia_port *port,
22
+ void *user_data,
23
+ void (*cb)(pjmedia_port *port,
24
+ void *usr_data));
25
+
26
+ PJ_DEF(pj_status_t) pjmedia_flite_port_speak( pjmedia_port *port,
27
+ const char *text,
28
+ unsigned options);
29
+
30
+ PJ_END_DECL
31
+
32
+ #endif /* __FLITE_PORT_H__ */
@@ -0,0 +1,263 @@
1
+ /* adapted from https://github.com/signalwire/freeswitch/blob/master/src/mod/asr_tts/mod_flite/mod_flite.c */
2
+
3
+ #include "flite_port.h"
4
+ #include <pjmedia/errno.h>
5
+ #include <pjmedia/port.h>
6
+ #include <pj/assert.h>
7
+ #include <pj/lock.h>
8
+ #include <pj/pool.h>
9
+ #include <pj/string.h>
10
+
11
+ #include "siplab_constants.h"
12
+
13
+ #include <flite/flite.h>
14
+
15
+ #define SIGNATURE PJMEDIA_SIGNATURE('f', 'l', 'i', 't')
16
+ #define THIS_FILE "flite_port.c"
17
+
18
+ #if 0
19
+ # define TRACE_(expr) PJ_LOG(4,expr)
20
+ #else
21
+ # define TRACE_(expr)
22
+ #endif
23
+
24
+ static pj_status_t flite_get_frame(pjmedia_port *port,
25
+ pjmedia_frame *frame);
26
+ static pj_status_t flite_on_destroy(pjmedia_port *port);
27
+
28
+
29
+ cst_voice *register_cmu_us_awb(void);
30
+ void unregister_cmu_us_awb(cst_voice * v);
31
+
32
+ cst_voice *register_cmu_us_kal(void);
33
+ void unregister_cmu_us_kal(cst_voice * v);
34
+
35
+ cst_voice *register_cmu_us_rms(void);
36
+ void unregister_cmu_us_rms(cst_voice * v);
37
+
38
+ cst_voice *register_cmu_us_slt(void);
39
+ void unregister_cmu_us_slt(cst_voice * v);
40
+
41
+ cst_voice *register_cmu_us_kal16(void);
42
+ void unregister_cmu_us_kal16(cst_voice * v);
43
+
44
+ static int initialized = 0;
45
+
46
+ static struct {
47
+ cst_voice *awb;
48
+ cst_voice *kal;
49
+ cst_voice *rms;
50
+ cst_voice *slt;
51
+ cst_voice *kal16;
52
+ } globals;
53
+
54
+ struct flite_t {
55
+ struct pjmedia_port base;
56
+ unsigned options;
57
+
58
+ cst_voice *v;
59
+ unsigned written_samples;
60
+ cst_wave *w;
61
+
62
+ pj_bool_t subscribed;
63
+ void (*cb)(pjmedia_port*, void*);
64
+ };
65
+
66
+ #define free_wave(w) if (w) {delete_wave(w) ; w = NULL; }
67
+ #define FLITE_BLOCK_SIZE 1024 * 32
68
+
69
+ /*
70
+ * Register a callback to be called when we reach the end of speech
71
+ */
72
+ PJ_DEF(pj_status_t) pjmedia_flite_port_set_eof_cb(pjmedia_port *port,
73
+ void *user_data,
74
+ void (*cb)(pjmedia_port *port,
75
+ void *usr_data))
76
+ {
77
+ struct flite_t *flite;
78
+
79
+ /* Sanity check */
80
+ PJ_ASSERT_RETURN(port, -PJ_EINVAL);
81
+
82
+ /* Check that this is really a flite port */
83
+ PJ_ASSERT_RETURN(port->info.signature == SIGNATURE, -PJ_EINVALIDOP);
84
+
85
+ flite = (struct flite_t*) port;
86
+
87
+ flite->base.port_data.pdata = user_data;
88
+ flite->cb = cb;
89
+
90
+ return PJ_SUCCESS;
91
+ }
92
+
93
+
94
+ static pj_status_t speech_on_event(pjmedia_event *event,
95
+ void *user_data)
96
+ {
97
+ struct flite_t *flite = (struct flite_t*)user_data;
98
+
99
+ if (event->type == PJMEDIA_EVENT_CALLBACK) {
100
+ if (flite->cb)
101
+ (*flite->cb)(&flite->base, flite->base.port_data.pdata);
102
+ }
103
+
104
+ return PJ_SUCCESS;
105
+ }
106
+
107
+ PJ_DEF(pj_status_t) pjmedia_flite_port_create( pj_pool_t *pool,
108
+ unsigned clock_rate,
109
+ unsigned channel_count,
110
+ unsigned samples_per_frame,
111
+ unsigned bits_per_sample,
112
+ const char *voice,
113
+ pjmedia_port **p_port)
114
+ {
115
+ struct flite_t *flite;
116
+ const pj_str_t name = pj_str("flite_data");
117
+
118
+ if(!initialized) {
119
+ flite_init();
120
+ globals.awb = register_cmu_us_awb();
121
+ globals.kal = register_cmu_us_kal();
122
+ globals.rms = register_cmu_us_rms();
123
+ globals.slt = register_cmu_us_slt();
124
+ globals.kal16 = register_cmu_us_kal16();
125
+ initialized = 1;
126
+ }
127
+
128
+ PJ_ASSERT_RETURN(pool && clock_rate && channel_count &&
129
+ samples_per_frame && bits_per_sample == 16 &&
130
+ p_port != NULL, PJ_EINVAL);
131
+
132
+ PJ_ASSERT_RETURN(pool && p_port, PJ_EINVAL);
133
+
134
+ flite = PJ_POOL_ZALLOC_T(pool, struct flite_t);
135
+ PJ_ASSERT_RETURN(pool != NULL, PJ_ENOMEM);
136
+
137
+ pjmedia_port_info_init(&flite->base.info, &name, SIGNATURE, clock_rate,
138
+ channel_count, bits_per_sample, samples_per_frame);
139
+
140
+ flite->base.get_frame = &flite_get_frame;
141
+ flite->base.on_destroy = &flite_on_destroy;
142
+
143
+ if (!strcasecmp(voice, "awb")) {
144
+ flite->v = globals.awb;
145
+ } else if (!strcasecmp(voice, "kal")) {
146
+ /* "kal" is 8kHz and the native rate is set to 16kHz
147
+ * so kal talks a little bit too fast ...
148
+ * for now: "symlink" kal to kal16
149
+ */ flite->v = globals.kal16;
150
+ } else if (!strcasecmp(voice, "rms")) {
151
+ flite->v = globals.rms;
152
+ } else if (!strcasecmp(voice, "slt")) {
153
+ flite->v = globals.slt;
154
+ } else if (!strcasecmp(voice, "kal16")) {
155
+ flite->v = globals.kal16;
156
+ } else {
157
+ TRACE_((THIS_FILE, "Invalid voice"));
158
+ return 0;
159
+ }
160
+
161
+ TRACE_((THIS_FILE, "flite_device created: %u/%u/%u/%u", clock_rate,
162
+ channel_count, samples_per_frame, bits_per_sample));
163
+
164
+ *p_port = &flite->base;
165
+ return PJ_SUCCESS;
166
+ }
167
+
168
+ PJ_DEF(pj_status_t) pjmedia_flite_port_speak( pjmedia_port *port,
169
+ const char *text,
170
+ unsigned options) {
171
+ struct flite_t *flite = (struct flite_t*)port;
172
+ if(flite->w) {
173
+ free_wave(flite->w);
174
+ }
175
+
176
+ flite->options = options;
177
+
178
+ flite->w = flite_text_to_wave(text, flite->v);
179
+ if ((unsigned)flite->w->sample_rate != PJMEDIA_PIA_SRATE(&port->info)) {
180
+ printf("resampling from %i to %i\n", flite->w->sample_rate, PJMEDIA_PIA_SRATE(&port->info));
181
+ cst_wave_resample(flite->w, PJMEDIA_PIA_SRATE(&port->info));
182
+ }
183
+ flite->written_samples = 0;
184
+
185
+ return PJ_SUCCESS;
186
+ }
187
+
188
+ // called when pjmedia needs data to be sent out
189
+ static pj_status_t flite_get_frame(pjmedia_port *port,
190
+ pjmedia_frame *frame) {
191
+
192
+ PJ_ASSERT_RETURN(port && frame, PJ_EINVAL);
193
+
194
+ struct flite_t *flite = (struct flite_t*)port;
195
+
196
+ if(!flite->w) {
197
+ printf("flite no data\n");
198
+ frame->type = PJMEDIA_FRAME_TYPE_NONE;
199
+ return PJ_SUCCESS;
200
+ }
201
+
202
+ printf("written_samples=%i num_samples=%i\n", flite->written_samples, flite->w->num_samples);
203
+ if (flite->written_samples + PJMEDIA_PIA_SPF(&port->info) > (unsigned)flite->w->num_samples) {
204
+ printf("flite end of speech\n");
205
+
206
+ if(flite->cb) {
207
+ if (!flite->subscribed) {
208
+ pj_status_t status = pjmedia_event_subscribe(NULL, &speech_on_event,
209
+ flite, flite);
210
+ flite->subscribed = (status == PJ_SUCCESS)? PJ_TRUE:
211
+ PJ_FALSE;
212
+ }
213
+
214
+ if (flite->subscribed) {
215
+ pjmedia_event event;
216
+
217
+ pjmedia_event_init(&event, PJMEDIA_EVENT_CALLBACK,
218
+ NULL, flite);
219
+ pjmedia_event_publish(NULL, flite, &event,
220
+ PJMEDIA_EVENT_PUBLISH_POST_EVENT);
221
+ }
222
+ }
223
+
224
+ pj_bool_t no_loop = (flite->options & PJMEDIA_SPEECH_NO_LOOP);
225
+
226
+ if(no_loop) {
227
+ free_wave(flite->w);
228
+ frame->type = PJMEDIA_FRAME_TYPE_NONE;
229
+ return PJ_SUCCESS;
230
+ } else {
231
+ flite->written_samples = 0;
232
+ }
233
+ }
234
+
235
+ memcpy(frame->buf, flite->w->samples + flite->written_samples, PJMEDIA_PIA_SPF(&port->info)*2);
236
+ flite->written_samples += PJMEDIA_PIA_SPF(&port->info);
237
+ frame->type = PJMEDIA_FRAME_TYPE_AUDIO;
238
+ printf("flite data written samples=%i\n", PJMEDIA_PIA_SPF(&port->info));
239
+
240
+ return PJ_SUCCESS;
241
+ }
242
+
243
+ /*
244
+ * Destroy port.
245
+ */
246
+ static pj_status_t flite_on_destroy(pjmedia_port *port)
247
+ {
248
+ printf("flite_on_destroy\n");
249
+
250
+ struct flite_t *flite = (struct flite_t*)port;
251
+
252
+ pj_assert(port->info.signature == SIGNATURE);
253
+
254
+ free_wave(flite->w);
255
+
256
+ if (flite->subscribed) {
257
+ pjmedia_event_unsubscribe(NULL, &speech_on_event, flite, flite);
258
+ flite->subscribed = PJ_FALSE;
259
+ }
260
+
261
+ return PJ_SUCCESS;
262
+ }
263
+