cannam@111
|
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
|
c@118
|
2 /*
|
c@118
|
3 Piper C++
|
c@118
|
4
|
c@118
|
5 An API for audio analysis and feature extraction plugins.
|
c@118
|
6
|
c@118
|
7 Centre for Digital Music, Queen Mary, University of London.
|
c@118
|
8 Copyright 2006-2016 Chris Cannam and QMUL.
|
c@118
|
9
|
c@118
|
10 Permission is hereby granted, free of charge, to any person
|
c@118
|
11 obtaining a copy of this software and associated documentation
|
c@118
|
12 files (the "Software"), to deal in the Software without
|
c@118
|
13 restriction, including without limitation the rights to use, copy,
|
c@118
|
14 modify, merge, publish, distribute, sublicense, and/or sell copies
|
c@118
|
15 of the Software, and to permit persons to whom the Software is
|
c@118
|
16 furnished to do so, subject to the following conditions:
|
c@118
|
17
|
c@118
|
18 The above copyright notice and this permission notice shall be
|
c@118
|
19 included in all copies or substantial portions of the Software.
|
c@118
|
20
|
c@118
|
21 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
c@118
|
22 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
c@118
|
23 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
c@118
|
24 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
|
c@118
|
25 ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
c@118
|
26 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
c@118
|
27 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
c@118
|
28
|
c@118
|
29 Except as contained in this notice, the names of the Centre for
|
c@118
|
30 Digital Music; Queen Mary, University of London; and Chris Cannam
|
c@118
|
31 shall not be used in advertising or otherwise to promote the sale,
|
c@118
|
32 use or other dealings in this Software without prior written
|
c@118
|
33 authorization.
|
c@118
|
34 */
|
c@96
|
35
|
c@96
|
36 #ifndef PIPER_CAPNP_CLIENT_H
|
c@96
|
37 #define PIPER_CAPNP_CLIENT_H
|
c@96
|
38
|
c@96
|
39 #include "Loader.h"
|
c@96
|
40 #include "PluginClient.h"
|
c@96
|
41 #include "PluginStub.h"
|
c@96
|
42 #include "SynchronousTransport.h"
|
c@96
|
43
|
c@96
|
44 #include "vamp-support/AssignedPluginHandleMapper.h"
|
c@96
|
45 #include "vamp-capnp/VampnProto.h"
|
c@96
|
46
|
c@134
|
47 #include <sstream>
|
c@134
|
48
|
c@96
|
49 #include <capnp/serialize.h>
|
c@96
|
50
|
c@144
|
51 //#define LOG_ENTRYPOINTS 1
|
c@142
|
52
|
c@142
|
53 #ifdef LOG_ENTRYPOINTS
|
c@142
|
54 #define LOG_E(x) log(x)
|
c@142
|
55 #else
|
c@142
|
56 #define LOG_E(x)
|
c@142
|
57 #endif
|
c@142
|
58
|
c@97
|
59 namespace piper_vamp {
|
c@97
|
60 namespace client {
|
c@96
|
61
|
c@100
|
62 /**
|
c@100
|
63 * Client for a request-response Piper server, i.e. using the
|
c@100
|
64 * RpcRequest/RpcResponse structures with a single process call rather
|
c@100
|
65 * than having individual RPC methods, with a synchronous transport
|
c@100
|
66 * such as a subprocess pipe arrangement. Only one request can be
|
c@100
|
67 * handled at a time. This class is thread-safe if and only if it is
|
c@100
|
68 * constructed with a thread-safe SynchronousTransport implementation.
|
c@100
|
69 */
|
c@96
|
70 class CapnpRRClient : public PluginClient,
|
c@118
|
71 public Loader
|
c@96
|
72 {
|
c@96
|
73 // unsigned to avoid undefined behaviour on possible wrap
|
c@96
|
74 typedef uint32_t ReqId;
|
c@96
|
75
|
c@96
|
76 class CompletenessChecker : public MessageCompletenessChecker {
|
c@96
|
77 public:
|
c@146
|
78 State check(const std::vector<char> &message) const override {
|
c@146
|
79
|
c@96
|
80 auto karr = toKJArray(message);
|
c@96
|
81 size_t words = karr.size();
|
c@96
|
82 size_t expected = capnp::expectedSizeInWordsFromPrefix(karr);
|
c@146
|
83
|
c@146
|
84 // Lacking a way to definitively check whether a message
|
c@146
|
85 // is valid or not, we would still like to trap obvious
|
c@146
|
86 // cases where a programming mistake results in garbage
|
c@146
|
87 // being returned from the server. We impose a limit on
|
c@146
|
88 // message size and, if a prefix is projected to exceed
|
c@146
|
89 // that limit, call it invalid. If an extractor wants to
|
c@146
|
90 // return a feature set greater than a gigaword in size,
|
c@146
|
91 // it'll just have to do it across multiple process calls.
|
c@146
|
92 size_t limit = size_t(1) << 30;
|
c@146
|
93
|
c@145
|
94 // cerr << "CompletenessChecker: message.size() = " << message.size()
|
c@146
|
95 // << ", words = " << words << ", limit = " << limit << ", expected = " << expected << endl;
|
c@146
|
96
|
c@96
|
97 if (words > expected) {
|
c@96
|
98 std::cerr << "WARNING: obtained more data than expected ("
|
c@96
|
99 << words << " " << sizeof(capnp::word)
|
c@96
|
100 << "-byte words, expected "
|
c@96
|
101 << expected << ")" << std::endl;
|
c@146
|
102 return Complete;
|
c@146
|
103 } else if (words == expected) {
|
c@146
|
104 return Complete;
|
c@146
|
105 } else if (expected > limit) {
|
c@146
|
106 return Invalid;
|
c@146
|
107 } else {
|
c@146
|
108 return Incomplete;
|
c@96
|
109 }
|
c@96
|
110 }
|
c@96
|
111 };
|
c@96
|
112
|
c@96
|
113 public:
|
c@134
|
114 CapnpRRClient(SynchronousTransport *transport, //!!! ownership? shared ptr?
|
c@134
|
115 LogCallback *logger) : // logger may be nullptr for cerr
|
c@134
|
116 m_logger(logger),
|
c@96
|
117 m_transport(transport),
|
c@96
|
118 m_completenessChecker(new CompletenessChecker) {
|
c@96
|
119 transport->setCompletenessChecker(m_completenessChecker);
|
c@96
|
120 }
|
c@96
|
121
|
c@96
|
122 ~CapnpRRClient() {
|
c@96
|
123 delete m_completenessChecker;
|
c@96
|
124 }
|
c@96
|
125
|
c@96
|
126 //!!! obviously, factor out all repetitive guff
|
c@96
|
127
|
c@96
|
128 //!!! list and load are supposed to be called by application code,
|
c@96
|
129 //!!! but the rest are only supposed to be called by the plugin --
|
c@96
|
130 //!!! sort out the api here
|
c@96
|
131
|
c@96
|
132 // Loader methods:
|
c@96
|
133
|
c@97
|
134 ListResponse
|
c@132
|
135 listPluginData(const ListRequest &req) override {
|
c@96
|
136
|
c@142
|
137 LOG_E("CapnpRRClient::listPluginData called");
|
c@142
|
138
|
c@96
|
139 if (!m_transport->isOK()) {
|
c@134
|
140 log("Piper server crashed or failed to start (caller should have checked this)");
|
c@126
|
141 throw std::runtime_error("Piper server crashed or failed to start");
|
c@96
|
142 }
|
c@96
|
143
|
c@96
|
144 capnp::MallocMessageBuilder message;
|
c@118
|
145 piper::RpcRequest::Builder builder = message.initRoot<piper::RpcRequest>();
|
c@131
|
146 VampnProto::buildRpcRequest_List(builder, req);
|
c@96
|
147 ReqId id = getId();
|
c@96
|
148 builder.getId().setNumber(id);
|
c@96
|
149
|
c@134
|
150 auto karr = call(message, "list", true);
|
c@96
|
151
|
c@96
|
152 capnp::FlatArrayMessageReader responseMessage(karr);
|
c@97
|
153 piper::RpcResponse::Reader reader = responseMessage.getRoot<piper::RpcResponse>();
|
c@96
|
154
|
c@97
|
155 checkResponseType(reader, piper::RpcResponse::Response::Which::LIST, id);
|
c@96
|
156
|
c@97
|
157 ListResponse lr;
|
c@96
|
158 VampnProto::readListResponse(lr, reader.getResponse().getList());
|
c@142
|
159
|
c@142
|
160 LOG_E("CapnpRRClient::listPluginData returning");
|
c@142
|
161
|
c@96
|
162 return lr;
|
c@96
|
163 }
|
c@96
|
164
|
c@97
|
165 LoadResponse
|
c@97
|
166 loadPlugin(const LoadRequest &req) override {
|
c@96
|
167
|
c@142
|
168 LOG_E("CapnpRRClient::loadPlugin called");
|
c@142
|
169
|
c@96
|
170 if (!m_transport->isOK()) {
|
c@134
|
171 log("Piper server crashed or failed to start (caller should have checked this)");
|
c@126
|
172 throw std::runtime_error("Piper server crashed or failed to start");
|
c@96
|
173 }
|
c@96
|
174
|
c@97
|
175 LoadResponse resp;
|
c@96
|
176 PluginHandleMapper::Handle handle = serverLoad(req.pluginKey,
|
c@96
|
177 req.inputSampleRate,
|
c@96
|
178 req.adapterFlags,
|
c@96
|
179 resp.staticData,
|
c@96
|
180 resp.defaultConfiguration);
|
c@96
|
181
|
c@96
|
182 Vamp::Plugin *plugin = new PluginStub(this,
|
c@96
|
183 req.pluginKey,
|
c@96
|
184 req.inputSampleRate,
|
c@96
|
185 req.adapterFlags,
|
c@96
|
186 resp.staticData,
|
c@96
|
187 resp.defaultConfiguration);
|
c@96
|
188
|
c@96
|
189 m_mapper.addPlugin(handle, plugin);
|
c@96
|
190
|
c@96
|
191 resp.plugin = plugin;
|
c@142
|
192
|
c@142
|
193 LOG_E("CapnpRRClient::loadPlugin returning");
|
c@142
|
194
|
c@96
|
195 return resp;
|
c@96
|
196 }
|
c@96
|
197
|
c@96
|
198 // PluginClient methods:
|
c@96
|
199
|
c@96
|
200 virtual
|
c@96
|
201 Vamp::Plugin::OutputList
|
c@96
|
202 configure(PluginStub *plugin,
|
c@97
|
203 PluginConfiguration config) override {
|
c@96
|
204
|
c@142
|
205 LOG_E("CapnpRRClient::configure called");
|
c@142
|
206
|
c@96
|
207 if (!m_transport->isOK()) {
|
c@134
|
208 log("Piper server crashed or failed to start (caller should have checked this)");
|
c@126
|
209 throw std::runtime_error("Piper server crashed or failed to start");
|
c@96
|
210 }
|
c@96
|
211
|
c@97
|
212 ConfigurationRequest request;
|
c@96
|
213 request.plugin = plugin;
|
c@96
|
214 request.configuration = config;
|
c@96
|
215
|
c@96
|
216 capnp::MallocMessageBuilder message;
|
c@97
|
217 piper::RpcRequest::Builder builder = message.initRoot<piper::RpcRequest>();
|
c@96
|
218
|
c@96
|
219 VampnProto::buildRpcRequest_Configure(builder, request, m_mapper);
|
c@96
|
220 ReqId id = getId();
|
c@96
|
221 builder.getId().setNumber(id);
|
c@96
|
222
|
c@134
|
223 auto karr = call(message, "configure", true);
|
c@96
|
224
|
c@96
|
225 capnp::FlatArrayMessageReader responseMessage(karr);
|
c@97
|
226 piper::RpcResponse::Reader reader = responseMessage.getRoot<piper::RpcResponse>();
|
c@96
|
227
|
c@96
|
228 //!!! handle (explicit) error case
|
c@96
|
229
|
c@97
|
230 checkResponseType(reader, piper::RpcResponse::Response::Which::CONFIGURE, id);
|
c@96
|
231
|
c@97
|
232 ConfigurationResponse cr;
|
c@96
|
233 VampnProto::readConfigurationResponse(cr,
|
c@96
|
234 reader.getResponse().getConfigure(),
|
c@96
|
235 m_mapper);
|
c@96
|
236
|
c@142
|
237 LOG_E("CapnpRRClient::configure returning");
|
c@142
|
238
|
c@96
|
239 return cr.outputs;
|
c@96
|
240 };
|
c@96
|
241
|
c@96
|
242 virtual
|
c@96
|
243 Vamp::Plugin::FeatureSet
|
c@96
|
244 process(PluginStub *plugin,
|
c@96
|
245 std::vector<std::vector<float> > inputBuffers,
|
c@96
|
246 Vamp::RealTime timestamp) override {
|
c@96
|
247
|
c@142
|
248 LOG_E("CapnpRRClient::process called");
|
c@142
|
249
|
c@96
|
250 if (!m_transport->isOK()) {
|
c@134
|
251 log("Piper server crashed or failed to start (caller should have checked this)");
|
c@126
|
252 throw std::runtime_error("Piper server crashed or failed to start");
|
c@96
|
253 }
|
c@96
|
254
|
c@97
|
255 ProcessRequest request;
|
c@96
|
256 request.plugin = plugin;
|
c@96
|
257 request.inputBuffers = inputBuffers;
|
c@96
|
258 request.timestamp = timestamp;
|
c@96
|
259
|
c@96
|
260 capnp::MallocMessageBuilder message;
|
c@97
|
261 piper::RpcRequest::Builder builder = message.initRoot<piper::RpcRequest>();
|
c@96
|
262 VampnProto::buildRpcRequest_Process(builder, request, m_mapper);
|
c@118
|
263 ReqId id = getId();
|
c@96
|
264 builder.getId().setNumber(id);
|
c@96
|
265
|
c@134
|
266 auto karr = call(message, "process", false);
|
c@96
|
267
|
c@96
|
268 capnp::FlatArrayMessageReader responseMessage(karr);
|
c@97
|
269 piper::RpcResponse::Reader reader = responseMessage.getRoot<piper::RpcResponse>();
|
c@96
|
270
|
c@96
|
271 //!!! handle (explicit) error case
|
c@96
|
272
|
c@97
|
273 checkResponseType(reader, piper::RpcResponse::Response::Which::PROCESS, id);
|
c@96
|
274
|
c@97
|
275 ProcessResponse pr;
|
c@96
|
276 VampnProto::readProcessResponse(pr,
|
c@96
|
277 reader.getResponse().getProcess(),
|
c@96
|
278 m_mapper);
|
c@96
|
279
|
c@142
|
280 LOG_E("CapnpRRClient::process returning");
|
c@142
|
281
|
c@96
|
282 return pr.features;
|
c@96
|
283 }
|
c@96
|
284
|
c@96
|
285 virtual Vamp::Plugin::FeatureSet
|
c@96
|
286 finish(PluginStub *plugin) override {
|
c@96
|
287
|
c@142
|
288 LOG_E("CapnpRRClient::finish called");
|
c@142
|
289
|
c@96
|
290 if (!m_transport->isOK()) {
|
c@134
|
291 log("Piper server crashed or failed to start (caller should have checked this)");
|
c@126
|
292 throw std::runtime_error("Piper server crashed or failed to start");
|
c@96
|
293 }
|
c@96
|
294
|
c@97
|
295 FinishRequest request;
|
c@96
|
296 request.plugin = plugin;
|
c@96
|
297
|
c@96
|
298 capnp::MallocMessageBuilder message;
|
c@97
|
299 piper::RpcRequest::Builder builder = message.initRoot<piper::RpcRequest>();
|
c@96
|
300
|
c@96
|
301 VampnProto::buildRpcRequest_Finish(builder, request, m_mapper);
|
c@96
|
302 ReqId id = getId();
|
c@96
|
303 builder.getId().setNumber(id);
|
c@96
|
304
|
c@134
|
305 auto karr = call(message, "finish", true);
|
c@96
|
306
|
c@96
|
307 capnp::FlatArrayMessageReader responseMessage(karr);
|
c@97
|
308 piper::RpcResponse::Reader reader = responseMessage.getRoot<piper::RpcResponse>();
|
c@96
|
309
|
c@96
|
310 //!!! handle (explicit) error case
|
c@96
|
311
|
c@97
|
312 checkResponseType(reader, piper::RpcResponse::Response::Which::FINISH, id);
|
c@96
|
313
|
c@97
|
314 FinishResponse pr;
|
c@96
|
315 VampnProto::readFinishResponse(pr,
|
c@96
|
316 reader.getResponse().getFinish(),
|
c@96
|
317 m_mapper);
|
c@96
|
318
|
c@96
|
319 m_mapper.removePlugin(m_mapper.pluginToHandle(plugin));
|
c@96
|
320
|
c@118
|
321 // Don't delete the plugin. It's the plugin that is supposed
|
c@118
|
322 // to be calling us here
|
c@96
|
323
|
c@142
|
324 LOG_E("CapnpRRClient::finish returning");
|
c@142
|
325
|
c@96
|
326 return pr.features;
|
c@96
|
327 }
|
c@96
|
328
|
c@96
|
329 virtual void
|
c@96
|
330 reset(PluginStub *plugin,
|
c@97
|
331 PluginConfiguration config) override {
|
c@96
|
332
|
c@96
|
333 // Reload the plugin on the server side, and configure it as requested
|
c@134
|
334
|
c@134
|
335 log("CapnpRRClient: reset() called, plugin will be closed and reloaded");
|
c@96
|
336
|
c@96
|
337 if (!m_transport->isOK()) {
|
c@134
|
338 log("Piper server crashed or failed to start (caller should have checked this)");
|
c@126
|
339 throw std::runtime_error("Piper server crashed or failed to start");
|
c@96
|
340 }
|
c@96
|
341
|
c@96
|
342 if (m_mapper.havePlugin(plugin)) {
|
c@96
|
343 (void)finish(plugin); // server-side unload
|
c@96
|
344 }
|
c@96
|
345
|
c@97
|
346 PluginStaticData psd;
|
c@97
|
347 PluginConfiguration defaultConfig;
|
c@96
|
348 PluginHandleMapper::Handle handle =
|
c@96
|
349 serverLoad(plugin->getPluginKey(),
|
c@96
|
350 plugin->getInputSampleRate(),
|
c@96
|
351 plugin->getAdapterFlags(),
|
c@96
|
352 psd, defaultConfig);
|
c@96
|
353
|
c@96
|
354 m_mapper.addPlugin(handle, plugin);
|
c@96
|
355
|
c@96
|
356 (void)configure(plugin, config);
|
c@96
|
357 }
|
c@96
|
358
|
c@96
|
359 private:
|
c@96
|
360 AssignedPluginHandleMapper m_mapper;
|
c@96
|
361 ReqId getId() {
|
c@96
|
362 //!!! todo: mutex
|
c@96
|
363 static ReqId m_nextId = 0;
|
c@96
|
364 return m_nextId++;
|
c@96
|
365 }
|
c@96
|
366
|
c@96
|
367 static
|
c@96
|
368 kj::Array<capnp::word>
|
c@96
|
369 toKJArray(const std::vector<char> &buffer) {
|
c@118
|
370 // We could do this whole thing with fewer copies, but let's
|
c@118
|
371 // see whether it matters first
|
c@96
|
372 size_t wordSize = sizeof(capnp::word);
|
c@118
|
373 size_t words = buffer.size() / wordSize;
|
c@118
|
374 kj::Array<capnp::word> karr(kj::heapArray<capnp::word>(words));
|
c@118
|
375 memcpy(karr.begin(), buffer.data(), words * wordSize);
|
c@118
|
376 return karr;
|
c@96
|
377 }
|
c@96
|
378
|
c@96
|
379 void
|
c@97
|
380 checkResponseType(const piper::RpcResponse::Reader &r,
|
c@97
|
381 piper::RpcResponse::Response::Which type,
|
c@96
|
382 ReqId id) {
|
c@96
|
383
|
c@96
|
384 if (r.getResponse().which() != type) {
|
c@134
|
385 std::ostringstream s;
|
c@134
|
386 s << "checkResponseType: wrong response type (received "
|
c@134
|
387 << int(r.getResponse().which()) << ", expected " << int(type) << ")";
|
c@134
|
388 log(s.str());
|
c@96
|
389 throw std::runtime_error("Wrong response type");
|
c@96
|
390 }
|
c@96
|
391 if (ReqId(r.getId().getNumber()) != id) {
|
c@134
|
392 std::ostringstream s;
|
c@134
|
393 s << "checkResponseType: wrong response id (received "
|
c@134
|
394 << r.getId().getNumber() << ", expected " << id << ")";
|
c@134
|
395 log(s.str());
|
c@96
|
396 throw std::runtime_error("Wrong response id");
|
c@96
|
397 }
|
c@96
|
398 }
|
c@96
|
399
|
c@96
|
400 kj::Array<capnp::word>
|
c@134
|
401 call(capnp::MallocMessageBuilder &message, std::string type, bool slow) {
|
c@96
|
402 auto arr = capnp::messageToFlatArray(message);
|
c@96
|
403 auto responseBuffer = m_transport->call(arr.asChars().begin(),
|
c@126
|
404 arr.asChars().size(),
|
c@134
|
405 type,
|
c@126
|
406 slow);
|
c@118
|
407 return toKJArray(responseBuffer);
|
c@96
|
408 }
|
c@96
|
409
|
c@96
|
410 PluginHandleMapper::Handle
|
c@96
|
411 serverLoad(std::string key, float inputSampleRate, int adapterFlags,
|
c@97
|
412 PluginStaticData &psd,
|
c@97
|
413 PluginConfiguration &defaultConfig) {
|
c@96
|
414
|
c@97
|
415 LoadRequest request;
|
c@96
|
416 request.pluginKey = key;
|
c@96
|
417 request.inputSampleRate = inputSampleRate;
|
c@96
|
418 request.adapterFlags = adapterFlags;
|
c@96
|
419
|
c@96
|
420 capnp::MallocMessageBuilder message;
|
c@97
|
421 piper::RpcRequest::Builder builder = message.initRoot<piper::RpcRequest>();
|
c@96
|
422
|
c@96
|
423 VampnProto::buildRpcRequest_Load(builder, request);
|
c@96
|
424 ReqId id = getId();
|
c@96
|
425 builder.getId().setNumber(id);
|
c@96
|
426
|
c@134
|
427 auto karr = call(message, "load", false);
|
c@96
|
428
|
c@96
|
429 //!!! ... --> will also need some way to kill this process
|
c@96
|
430 //!!! (from another thread)
|
c@96
|
431
|
c@96
|
432 capnp::FlatArrayMessageReader responseMessage(karr);
|
c@97
|
433 piper::RpcResponse::Reader reader = responseMessage.getRoot<piper::RpcResponse>();
|
c@96
|
434
|
c@96
|
435 //!!! handle (explicit) error case
|
c@96
|
436
|
c@97
|
437 checkResponseType(reader, piper::RpcResponse::Response::Which::LOAD, id);
|
c@96
|
438
|
c@97
|
439 const piper::LoadResponse::Reader &lr = reader.getResponse().getLoad();
|
c@96
|
440 VampnProto::readExtractorStaticData(psd, lr.getStaticData());
|
c@96
|
441 VampnProto::readConfiguration(defaultConfig, lr.getDefaultConfiguration());
|
c@96
|
442 return lr.getHandle();
|
c@96
|
443 };
|
c@96
|
444
|
c@96
|
445 private:
|
c@134
|
446 LogCallback *m_logger;
|
c@96
|
447 SynchronousTransport *m_transport; //!!! I don't own this, but should I?
|
c@96
|
448 CompletenessChecker *m_completenessChecker; // I own this
|
c@134
|
449
|
c@134
|
450 void log(std::string message) const {
|
c@134
|
451 if (m_logger) m_logger->log(message);
|
c@134
|
452 else std::cerr << message << std::endl;
|
c@134
|
453 }
|
c@96
|
454 };
|
c@96
|
455
|
c@96
|
456 }
|
c@96
|
457 }
|
c@96
|
458
|
c@96
|
459 #endif
|