Jack2 1.9.8
|
00001 /* 00002 Copyright (C) 2011 Devin Anderson 00003 00004 This program is free software; you can redistribute it and/or modify 00005 it under the terms of the GNU General Public License as published by 00006 the Free Software Foundation; either version 2 of the License, or 00007 (at your option) any later version. 00008 00009 This program is distributed in the hope that it will be useful, 00010 but WITHOUT ANY WARRANTY; without even the implied warranty of 00011 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00012 GNU General Public License for more details. 00013 00014 You should have received a copy of the GNU General Public License 00015 along with this program; if not, write to the Free Software 00016 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 00017 00018 */ 00019 00020 #include <memory> 00021 #include <new> 00022 #include <stdexcept> 00023 00024 #include <alsa/asoundlib.h> 00025 00026 #include "JackALSARawMidiDriver.h" 00027 #include "JackALSARawMidiUtil.h" 00028 #include "JackEngineControl.h" 00029 #include "JackError.h" 00030 #include "JackMidiUtil.h" 00031 00032 using Jack::JackALSARawMidiDriver; 00033 00034 JackALSARawMidiDriver::JackALSARawMidiDriver(const char *name, 00035 const char *alias, 00036 JackLockedEngine *engine, 00037 JackSynchro *table): 00038 JackMidiDriver(name, alias, engine, table) 00039 { 00040 thread = new JackThread(this); 00041 fds[0] = -1; 00042 fds[1] = -1; 00043 input_ports = 0; 00044 output_ports = 0; 00045 output_port_timeouts = 0; 00046 poll_fds = 0; 00047 } 00048 00049 JackALSARawMidiDriver::~JackALSARawMidiDriver() 00050 { 00051 delete thread; 00052 } 00053 00054 int 00055 JackALSARawMidiDriver::Attach() 00056 { 00057 const char *alias; 00058 jack_nframes_t buffer_size = fEngineControl->fBufferSize; 00059 jack_port_id_t index; 00060 jack_nframes_t latency = buffer_size; 00061 jack_latency_range_t latency_range; 00062 const char *name; 00063 JackPort *port; 00064 latency_range.max = latency; 00065 latency_range.min = latency; 00066 for (int i = 0; i < fCaptureChannels; i++) { 00067 JackALSARawMidiInputPort *input_port = input_ports[i]; 00068 name = input_port->GetName(); 00069 fEngine->PortRegister(fClientControl.fRefNum, name, 00070 JACK_DEFAULT_MIDI_TYPE, 00071 CaptureDriverFlags, buffer_size, &index); 00072 if (index == NO_PORT) { 00073 jack_error("JackALSARawMidiDriver::Attach - cannot register input " 00074 "port with name '%s'.", name); 00075 // XX: Do we need to deallocate ports? 00076 return -1; 00077 } 00078 alias = input_port->GetAlias(); 00079 port = fGraphManager->GetPort(index); 00080 port->SetAlias(alias); 00081 port->SetLatencyRange(JackCaptureLatency, &latency_range); 00082 fCapturePortList[i] = index; 00083 00084 jack_info("JackALSARawMidiDriver::Attach - input port registered " 00085 "(name='%s', alias='%s').", name, alias); 00086 } 00087 if (! fEngineControl->fSyncMode) { 00088 latency += buffer_size; 00089 latency_range.max = latency; 00090 latency_range.min = latency; 00091 } 00092 for (int i = 0; i < fPlaybackChannels; i++) { 00093 JackALSARawMidiOutputPort *output_port = output_ports[i]; 00094 name = output_port->GetName(); 00095 fEngine->PortRegister(fClientControl.fRefNum, name, 00096 JACK_DEFAULT_MIDI_TYPE, 00097 PlaybackDriverFlags, buffer_size, &index); 00098 if (index == NO_PORT) { 00099 jack_error("JackALSARawMidiDriver::Attach - cannot register " 00100 "output port with name '%s'.", name); 00101 // XX: Do we need to deallocate ports? 00102 return -1; 00103 } 00104 alias = output_port->GetAlias(); 00105 port = fGraphManager->GetPort(index); 00106 port->SetAlias(alias); 00107 port->SetLatencyRange(JackPlaybackLatency, &latency_range); 00108 fPlaybackPortList[i] = index; 00109 00110 jack_info("JackALSARawMidiDriver::Attach - output port registered " 00111 "(name='%s', alias='%s').", name, alias); 00112 } 00113 return 0; 00114 } 00115 00116 int 00117 JackALSARawMidiDriver::Close() 00118 { 00119 // Generic MIDI driver close 00120 int result = JackMidiDriver::Close(); 00121 00122 if (input_ports) { 00123 for (int i = 0; i < fCaptureChannels; i++) { 00124 delete input_ports[i]; 00125 } 00126 delete[] input_ports; 00127 input_ports = 0; 00128 } 00129 if (output_ports) { 00130 for (int i = 0; i < fPlaybackChannels; i++) { 00131 delete output_ports[i]; 00132 } 00133 delete[] output_ports; 00134 output_ports = 0; 00135 } 00136 return result; 00137 } 00138 00139 bool 00140 JackALSARawMidiDriver::Execute() 00141 { 00142 jack_nframes_t timeout_frame = 0; 00143 for (;;) { 00144 struct timespec timeout; 00145 struct timespec *timeout_ptr; 00146 if (! timeout_frame) { 00147 timeout_ptr = 0; 00148 } else { 00149 00150 // The timeout value is relative to the time that 00151 // 'GetMicroSeconds()' is called, not the time that 'poll()' is 00152 // called. This means that the amount of time that passes between 00153 // 'GetMicroSeconds()' and 'ppoll()' is time that will be lost 00154 // while waiting for 'poll() to timeout. 00155 // 00156 // I tried to replace the timeout with a 'timerfd' with absolute 00157 // times, but, strangely, it actually slowed things down, and made 00158 // the code a lot more complicated. 00159 // 00160 // I wonder about using the 'epoll' interface instead of 'ppoll()'. 00161 // The problem with the 'epoll' interface is that the timeout 00162 // resolution of 'epoll_wait()' is set in milliseconds. We need 00163 // microsecond resolution. Without microsecond resolution, we 00164 // impose the same jitter as USB MIDI. 00165 // 00166 // Another problem is that 'ppoll()' returns later than the wait 00167 // time. The problem can be minimized with high precision timers. 00168 00169 timeout_ptr = &timeout; 00170 jack_time_t next_time = GetTimeFromFrames(timeout_frame); 00171 jack_time_t now = GetMicroSeconds(); 00172 if (next_time <= now) { 00173 timeout.tv_sec = 0; 00174 timeout.tv_nsec = 0; 00175 } else { 00176 jack_time_t wait_time = next_time - now; 00177 timeout.tv_sec = wait_time / 1000000; 00178 timeout.tv_nsec = (wait_time % 1000000) * 1000; 00179 } 00180 } 00181 int poll_result = ppoll(poll_fds, poll_fd_count, timeout_ptr, 0); 00182 00183 // Getting the current frame value here allows us to use it for 00184 // incoming MIDI bytes. This makes sense, as the data has already 00185 // arrived at this point. 00186 jack_nframes_t current_frame = GetCurrentFrame(); 00187 00188 if (poll_result == -1) { 00189 if (errno == EINTR) { 00190 continue; 00191 } 00192 jack_error("JackALSARawMidiDriver::Execute - poll error: %s", 00193 strerror(errno)); 00194 break; 00195 } 00196 jack_nframes_t port_timeout; 00197 timeout_frame = 0; 00198 if (! poll_result) { 00199 00200 // No I/O events occurred. So, only handle timeout events on 00201 // output ports. 00202 00203 for (int i = 0; i < fPlaybackChannels; i++) { 00204 port_timeout = output_port_timeouts[i]; 00205 if (port_timeout && (port_timeout <= current_frame)) { 00206 if (! output_ports[i]->ProcessPollEvents(false, true, 00207 &port_timeout)) { 00208 jack_error("JackALSARawMidiDriver::Execute - a fatal " 00209 "error occurred while processing ALSA " 00210 "output events."); 00211 goto cleanup; 00212 } 00213 output_port_timeouts[i] = port_timeout; 00214 } 00215 if (port_timeout && ((! timeout_frame) || 00216 (port_timeout < timeout_frame))) { 00217 timeout_frame = port_timeout; 00218 } 00219 } 00220 continue; 00221 } 00222 00223 // See if it's time to shutdown. 00224 00225 unsigned short revents = poll_fds[0].revents; 00226 if (revents) { 00227 if (revents & (~ POLLHUP)) { 00228 jack_error("JackALSARawMidiDriver::Execute - unexpected poll " 00229 "event on pipe file descriptor."); 00230 } 00231 break; 00232 } 00233 00234 // Handle I/O events *and* timeout events on output ports. 00235 00236 for (int i = 0; i < fPlaybackChannels; i++) { 00237 port_timeout = output_port_timeouts[i]; 00238 bool timeout = port_timeout && (port_timeout <= current_frame); 00239 if (! output_ports[i]->ProcessPollEvents(true, timeout, 00240 &port_timeout)) { 00241 jack_error("JackALSARawMidiDriver::Execute - a fatal error " 00242 "occurred while processing ALSA output events."); 00243 goto cleanup; 00244 } 00245 output_port_timeouts[i] = port_timeout; 00246 if (port_timeout && ((! timeout_frame) || 00247 (port_timeout < timeout_frame))) { 00248 timeout_frame = port_timeout; 00249 } 00250 } 00251 00252 // Handle I/O events on input ports. We handle these last because we 00253 // already computed the arrival time above, and will impose a delay on 00254 // the events by 'period-size' frames anyway, which gives us a bit of 00255 // borrowed time. 00256 00257 for (int i = 0; i < fCaptureChannels; i++) { 00258 if (! input_ports[i]->ProcessPollEvents(current_frame)) { 00259 jack_error("JackALSARawMidiDriver::Execute - a fatal error " 00260 "occurred while processing ALSA input events."); 00261 goto cleanup; 00262 } 00263 } 00264 } 00265 cleanup: 00266 close(fds[0]); 00267 fds[0] = -1; 00268 00269 jack_info("JackALSARawMidiDriver::Execute - ALSA thread exiting."); 00270 00271 return false; 00272 } 00273 00274 void 00275 JackALSARawMidiDriver:: 00276 FreeDeviceInfo(std::vector<snd_rawmidi_info_t *> *in_info_list, 00277 std::vector<snd_rawmidi_info_t *> *out_info_list) 00278 { 00279 size_t length = in_info_list->size(); 00280 for (size_t i = 0; i < length; i++) { 00281 snd_rawmidi_info_free(in_info_list->at(i)); 00282 } 00283 length = out_info_list->size(); 00284 for (size_t i = 0; i < length; i++) { 00285 snd_rawmidi_info_free(out_info_list->at(i)); 00286 } 00287 } 00288 00289 void 00290 JackALSARawMidiDriver:: 00291 GetDeviceInfo(snd_ctl_t *control, snd_rawmidi_info_t *info, 00292 std::vector<snd_rawmidi_info_t *> *info_list) 00293 { 00294 snd_rawmidi_info_set_subdevice(info, 0); 00295 int code = snd_ctl_rawmidi_info(control, info); 00296 if (code) { 00297 if (code != -ENOENT) { 00298 HandleALSAError("GetDeviceInfo", "snd_ctl_rawmidi_info", code); 00299 } 00300 return; 00301 } 00302 unsigned int count = snd_rawmidi_info_get_subdevices_count(info); 00303 for (unsigned int i = 0; i < count; i++) { 00304 snd_rawmidi_info_set_subdevice(info, i); 00305 int code = snd_ctl_rawmidi_info(control, info); 00306 if (code) { 00307 HandleALSAError("GetDeviceInfo", "snd_ctl_rawmidi_info", code); 00308 continue; 00309 } 00310 snd_rawmidi_info_t *info_copy; 00311 code = snd_rawmidi_info_malloc(&info_copy); 00312 if (code) { 00313 HandleALSAError("GetDeviceInfo", "snd_rawmidi_info_malloc", code); 00314 continue; 00315 } 00316 snd_rawmidi_info_copy(info_copy, info); 00317 try { 00318 info_list->push_back(info_copy); 00319 } catch (std::bad_alloc &e) { 00320 snd_rawmidi_info_free(info_copy); 00321 jack_error("JackALSARawMidiDriver::GetDeviceInfo - " 00322 "std::vector::push_back: %s", e.what()); 00323 } 00324 } 00325 } 00326 00327 void 00328 JackALSARawMidiDriver::HandleALSAError(const char *driver_func, 00329 const char *alsa_func, int code) 00330 { 00331 jack_error("JackALSARawMidiDriver::%s - %s: %s", driver_func, alsa_func, 00332 snd_strerror(code)); 00333 } 00334 00335 bool 00336 JackALSARawMidiDriver::Init() 00337 { 00338 set_threaded_log_function(); 00339 if (thread->AcquireSelfRealTime(fEngineControl->fServerPriority + 1)) { 00340 jack_error("JackALSARawMidiDriver::Init - could not acquire realtime " 00341 "scheduling. Continuing anyway."); 00342 } 00343 return true; 00344 } 00345 00346 int 00347 JackALSARawMidiDriver::Open(bool capturing, bool playing, int in_channels, 00348 int out_channels, bool monitor, 00349 const char *capture_driver_name, 00350 const char *playback_driver_name, 00351 jack_nframes_t capture_latency, 00352 jack_nframes_t playback_latency) 00353 { 00354 snd_rawmidi_info_t *info; 00355 int code = snd_rawmidi_info_malloc(&info); 00356 if (code) { 00357 HandleALSAError("Open", "snd_rawmidi_info_malloc", code); 00358 return -1; 00359 } 00360 std::vector<snd_rawmidi_info_t *> in_info_list; 00361 std::vector<snd_rawmidi_info_t *> out_info_list; 00362 for (int card = -1;;) { 00363 int code = snd_card_next(&card); 00364 if (code) { 00365 HandleALSAError("Open", "snd_card_next", code); 00366 continue; 00367 } 00368 if (card == -1) { 00369 break; 00370 } 00371 char name[32]; 00372 snprintf(name, sizeof(name), "hw:%d", card); 00373 snd_ctl_t *control; 00374 code = snd_ctl_open(&control, name, SND_CTL_NONBLOCK); 00375 if (code) { 00376 HandleALSAError("Open", "snd_ctl_open", code); 00377 continue; 00378 } 00379 for (int device = -1;;) { 00380 code = snd_ctl_rawmidi_next_device(control, &device); 00381 if (code) { 00382 HandleALSAError("Open", "snd_ctl_rawmidi_next_device", code); 00383 continue; 00384 } 00385 if (device == -1) { 00386 break; 00387 } 00388 snd_rawmidi_info_set_device(info, device); 00389 snd_rawmidi_info_set_stream(info, SND_RAWMIDI_STREAM_INPUT); 00390 GetDeviceInfo(control, info, &in_info_list); 00391 snd_rawmidi_info_set_stream(info, SND_RAWMIDI_STREAM_OUTPUT); 00392 GetDeviceInfo(control, info, &out_info_list); 00393 } 00394 snd_ctl_close(control); 00395 } 00396 snd_rawmidi_info_free(info); 00397 size_t potential_inputs = in_info_list.size(); 00398 size_t potential_outputs = out_info_list.size(); 00399 if (! (potential_inputs || potential_outputs)) { 00400 jack_error("JackALSARawMidiDriver::Open - no ALSA raw MIDI input or " 00401 "output ports found."); 00402 FreeDeviceInfo(&in_info_list, &out_info_list); 00403 return -1; 00404 } 00405 size_t num_inputs = 0; 00406 size_t num_outputs = 0; 00407 if (potential_inputs) { 00408 try { 00409 input_ports = new JackALSARawMidiInputPort *[potential_inputs]; 00410 } catch (std::exception e) { 00411 jack_error("JackALSARawMidiDriver::Open - while creating input " 00412 "port array: %s", e.what()); 00413 FreeDeviceInfo(&in_info_list, &out_info_list); 00414 return -1; 00415 } 00416 } 00417 if (potential_outputs) { 00418 try { 00419 output_ports = new JackALSARawMidiOutputPort *[potential_outputs]; 00420 } catch (std::exception e) { 00421 jack_error("JackALSARawMidiDriver::Open - while creating output " 00422 "port array: %s", e.what()); 00423 FreeDeviceInfo(&in_info_list, &out_info_list); 00424 goto delete_input_ports; 00425 } 00426 } 00427 for (size_t i = 0; i < potential_inputs; i++) { 00428 snd_rawmidi_info_t *info = in_info_list.at(i); 00429 try { 00430 input_ports[num_inputs] = new JackALSARawMidiInputPort(info, i); 00431 num_inputs++; 00432 } catch (std::exception e) { 00433 jack_error("JackALSARawMidiDriver::Open - while creating new " 00434 "JackALSARawMidiInputPort: %s", e.what()); 00435 } 00436 snd_rawmidi_info_free(info); 00437 } 00438 for (size_t i = 0; i < potential_outputs; i++) { 00439 snd_rawmidi_info_t *info = out_info_list.at(i); 00440 try { 00441 output_ports[num_outputs] = new JackALSARawMidiOutputPort(info, i); 00442 num_outputs++; 00443 } catch (std::exception e) { 00444 jack_error("JackALSARawMidiDriver::Open - while creating new " 00445 "JackALSARawMidiOutputPort: %s", e.what()); 00446 } 00447 snd_rawmidi_info_free(info); 00448 } 00449 if (! (num_inputs || num_outputs)) { 00450 jack_error("JackALSARawMidiDriver::Open - none of the potential " 00451 "inputs or outputs were successfully opened."); 00452 } else if (JackMidiDriver::Open(capturing, playing, num_inputs, 00453 num_outputs, monitor, capture_driver_name, 00454 playback_driver_name, capture_latency, 00455 playback_latency)) { 00456 jack_error("JackALSARawMidiDriver::Open - JackMidiDriver::Open error"); 00457 } else { 00458 return 0; 00459 } 00460 if (output_ports) { 00461 for (size_t i = 0; i < num_outputs; i++) { 00462 delete output_ports[i]; 00463 } 00464 delete[] output_ports; 00465 output_ports = 0; 00466 } 00467 delete_input_ports: 00468 if (input_ports) { 00469 for (size_t i = 0; i < num_inputs; i++) { 00470 delete input_ports[i]; 00471 } 00472 delete[] input_ports; 00473 input_ports = 0; 00474 } 00475 return -1; 00476 } 00477 00478 int 00479 JackALSARawMidiDriver::Read() 00480 { 00481 jack_nframes_t buffer_size = fEngineControl->fBufferSize; 00482 for (int i = 0; i < fCaptureChannels; i++) { 00483 if (! input_ports[i]->ProcessJack(GetInputBuffer(i), buffer_size)) { 00484 return -1; 00485 } 00486 } 00487 return 0; 00488 } 00489 00490 int 00491 JackALSARawMidiDriver::Start() 00492 { 00493 00494 jack_info("JackALSARawMidiDriver::Start - Starting 'alsarawmidi' driver."); 00495 00496 JackMidiDriver::Start(); 00497 poll_fd_count = 1; 00498 for (int i = 0; i < fCaptureChannels; i++) { 00499 poll_fd_count += input_ports[i]->GetPollDescriptorCount(); 00500 } 00501 for (int i = 0; i < fPlaybackChannels; i++) { 00502 poll_fd_count += output_ports[i]->GetPollDescriptorCount(); 00503 } 00504 try { 00505 poll_fds = new pollfd[poll_fd_count]; 00506 } catch (std::exception e) { 00507 jack_error("JackALSARawMidiDriver::Start - creating poll descriptor " 00508 "structures failed: %s", e.what()); 00509 return -1; 00510 } 00511 if (fPlaybackChannels) { 00512 try { 00513 output_port_timeouts = new jack_nframes_t[fPlaybackChannels]; 00514 } catch (std::exception e) { 00515 jack_error("JackALSARawMidiDriver::Start - creating array for " 00516 "output port timeout values failed: %s", e.what()); 00517 goto free_poll_descriptors; 00518 } 00519 } 00520 struct pollfd *poll_fd_iter; 00521 try { 00522 CreateNonBlockingPipe(fds); 00523 } catch (std::exception e) { 00524 jack_error("JackALSARawMidiDriver::Start - while creating wake pipe: " 00525 "%s", e.what()); 00526 goto free_output_port_timeouts; 00527 } 00528 poll_fds[0].events = POLLERR | POLLIN | POLLNVAL; 00529 poll_fds[0].fd = fds[0]; 00530 poll_fd_iter = poll_fds + 1; 00531 for (int i = 0; i < fCaptureChannels; i++) { 00532 JackALSARawMidiInputPort *input_port = input_ports[i]; 00533 input_port->PopulatePollDescriptors(poll_fd_iter); 00534 poll_fd_iter += input_port->GetPollDescriptorCount(); 00535 } 00536 for (int i = 0; i < fPlaybackChannels; i++) { 00537 JackALSARawMidiOutputPort *output_port = output_ports[i]; 00538 output_port->PopulatePollDescriptors(poll_fd_iter); 00539 poll_fd_iter += output_port->GetPollDescriptorCount(); 00540 output_port_timeouts[i] = 0; 00541 } 00542 00543 jack_info("JackALSARawMidiDriver::Start - starting ALSA thread ..."); 00544 00545 if (! thread->StartSync()) { 00546 00547 jack_info("JackALSARawMidiDriver::Start - started ALSA thread."); 00548 00549 return 0; 00550 } 00551 jack_error("JackALSARawMidiDriver::Start - failed to start MIDI " 00552 "processing thread."); 00553 00554 DestroyNonBlockingPipe(fds); 00555 fds[1] = -1; 00556 fds[0] = -1; 00557 free_output_port_timeouts: 00558 delete[] output_port_timeouts; 00559 output_port_timeouts = 0; 00560 free_poll_descriptors: 00561 delete[] poll_fds; 00562 poll_fds = 0; 00563 return -1; 00564 } 00565 00566 int 00567 JackALSARawMidiDriver::Stop() 00568 { 00569 jack_info("JackALSARawMidiDriver::Stop - stopping 'alsarawmidi' driver."); 00570 JackMidiDriver::Stop(); 00571 00572 if (fds[1] != -1) { 00573 close(fds[1]); 00574 fds[1] = -1; 00575 } 00576 int result; 00577 const char *verb; 00578 switch (thread->GetStatus()) { 00579 case JackThread::kIniting: 00580 case JackThread::kStarting: 00581 result = thread->Kill(); 00582 verb = "kill"; 00583 break; 00584 case JackThread::kRunning: 00585 result = thread->Stop(); 00586 verb = "stop"; 00587 break; 00588 default: 00589 result = 0; 00590 verb = 0; 00591 } 00592 if (fds[0] != -1) { 00593 close(fds[0]); 00594 fds[0] = -1; 00595 } 00596 if (output_port_timeouts) { 00597 delete[] output_port_timeouts; 00598 output_port_timeouts = 0; 00599 } 00600 if (poll_fds) { 00601 delete[] poll_fds; 00602 poll_fds = 0; 00603 } 00604 if (result) { 00605 jack_error("JackALSARawMidiDriver::Stop - could not %s MIDI " 00606 "processing thread.", verb); 00607 } 00608 return result; 00609 } 00610 00611 int 00612 JackALSARawMidiDriver::Write() 00613 { 00614 jack_nframes_t buffer_size = fEngineControl->fBufferSize; 00615 for (int i = 0; i < fPlaybackChannels; i++) { 00616 if (! output_ports[i]->ProcessJack(GetOutputBuffer(i), buffer_size)) { 00617 return -1; 00618 } 00619 } 00620 return 0; 00621 } 00622 00623 #ifdef __cplusplus 00624 extern "C" { 00625 #endif 00626 00627 SERVER_EXPORT jack_driver_desc_t * 00628 driver_get_descriptor() 00629 { 00630 // X: There could be parameters here regarding setting I/O buffer 00631 // sizes. I don't think MIDI drivers can accept parameters right 00632 // now without being set as the main driver. 00633 00634 return jack_driver_descriptor_construct("alsarawmidi", JackDriverSlave, "Alternative ALSA raw MIDI backend.", NULL); 00635 } 00636 00637 SERVER_EXPORT Jack::JackDriverClientInterface * 00638 driver_initialize(Jack::JackLockedEngine *engine, Jack::JackSynchro *table, 00639 const JSList *params) 00640 { 00641 Jack::JackDriverClientInterface *driver = 00642 new Jack::JackALSARawMidiDriver("system_midi", "alsarawmidi", 00643 engine, table); 00644 if (driver->Open(1, 1, 0, 0, false, "midi in", "midi out", 0, 0)) { 00645 delete driver; 00646 driver = 0; 00647 } 00648 return driver; 00649 } 00650 00651 #ifdef __cplusplus 00652 } 00653 #endif