cubeb_sun.c (19285B)
1 /* 2 * Copyright © 2019-2020 Nia Alarie <nia@NetBSD.org> 3 * 4 * This program is made available under an ISC-style license. See the 5 * accompanying file LICENSE for details. 6 */ 7 #include "cubeb-internal.h" 8 #include "cubeb/cubeb.h" 9 #include "cubeb_tracing.h" 10 #include <fcntl.h> 11 #include <limits.h> 12 #include <pthread.h> 13 #include <stdbool.h> 14 #include <stdio.h> 15 #include <stdlib.h> 16 #include <string.h> 17 #include <sys/audioio.h> 18 #include <sys/ioctl.h> 19 #include <unistd.h> 20 21 /* Default to 4 + 1 for the default device. */ 22 #ifndef SUN_DEVICE_COUNT 23 #define SUN_DEVICE_COUNT (5) 24 #endif 25 26 /* Supported well by most hardware. */ 27 #ifndef SUN_PREFER_RATE 28 #define SUN_PREFER_RATE (48000) 29 #endif 30 31 /* Standard acceptable minimum. */ 32 #ifndef SUN_LATENCY_MS 33 #define SUN_LATENCY_MS (40) 34 #endif 35 36 #ifndef SUN_DEFAULT_DEVICE 37 #define SUN_DEFAULT_DEVICE "/dev/audio" 38 #endif 39 40 #ifndef SUN_BUFFER_FRAMES 41 #define SUN_BUFFER_FRAMES (32) 42 #endif 43 44 /* 45 * Supported on NetBSD regardless of hardware. 46 */ 47 48 #ifndef SUN_MAX_CHANNELS 49 #ifdef __NetBSD__ 50 #define SUN_MAX_CHANNELS (12) 51 #else 52 #define SUN_MAX_CHANNELS (2) 53 #endif 54 #endif 55 56 #ifndef SUN_MIN_RATE 57 #define SUN_MIN_RATE (1000) 58 #endif 59 60 #ifndef SUN_MAX_RATE 61 #define SUN_MAX_RATE (192000) 62 #endif 63 64 static struct cubeb_ops const sun_ops; 65 66 struct cubeb { 67 struct cubeb_ops const * ops; 68 }; 69 70 struct sun_stream { 71 char name[32]; 72 int fd; 73 void * buf; 74 struct audio_info info; 75 unsigned frame_size; /* precision in bytes * channels */ 76 bool floating; 77 }; 78 79 struct cubeb_stream { 80 struct cubeb * context; 81 void * user_ptr; 82 pthread_t thread; 83 pthread_mutex_t mutex; /* protects running, volume, frames_written */ 84 bool running; 85 float volume; 86 struct sun_stream play; 87 struct sun_stream record; 88 cubeb_data_callback data_cb; 89 cubeb_state_callback state_cb; 90 uint64_t frames_written; 91 uint64_t blocks_written; 92 }; 93 94 int 95 sun_init(cubeb ** context, char const * context_name) 96 { 97 cubeb * c; 98 99 (void)context_name; 100 if ((c = calloc(1, sizeof(cubeb))) == NULL) { 101 return CUBEB_ERROR; 102 } 103 c->ops = &sun_ops; 104 *context = c; 105 return CUBEB_OK; 106 } 107 108 static void 109 sun_destroy(cubeb * context) 110 { 111 free(context); 112 } 113 114 static char const * 115 sun_get_backend_id(cubeb * context) 116 { 117 return "sun"; 118 } 119 120 static int 121 sun_get_preferred_sample_rate(cubeb * context, uint32_t * rate) 122 { 123 (void)context; 124 125 *rate = SUN_PREFER_RATE; 126 return CUBEB_OK; 127 } 128 129 static int 130 sun_get_max_channel_count(cubeb * context, uint32_t * max_channels) 131 { 132 (void)context; 133 134 *max_channels = SUN_MAX_CHANNELS; 135 return CUBEB_OK; 136 } 137 138 static int 139 sun_get_min_latency(cubeb * context, cubeb_stream_params params, 140 uint32_t * latency_frames) 141 { 142 (void)context; 143 144 *latency_frames = SUN_LATENCY_MS * params.rate / 1000; 145 return CUBEB_OK; 146 } 147 148 static int 149 sun_get_hwinfo(const char * device, struct audio_info * format, int * props, 150 struct audio_device * dev) 151 { 152 int fd = -1; 153 154 if ((fd = open(device, O_RDONLY)) == -1) { 155 goto error; 156 } 157 #ifdef AUDIO_GETFORMAT 158 if (ioctl(fd, AUDIO_GETFORMAT, format) != 0) { 159 goto error; 160 } 161 #endif 162 #ifdef AUDIO_GETPROPS 163 if (ioctl(fd, AUDIO_GETPROPS, props) != 0) { 164 goto error; 165 } 166 #endif 167 if (ioctl(fd, AUDIO_GETDEV, dev) != 0) { 168 goto error; 169 } 170 close(fd); 171 return CUBEB_OK; 172 error: 173 if (fd != -1) { 174 close(fd); 175 } 176 return CUBEB_ERROR; 177 } 178 179 /* 180 * XXX: PR kern/54264 181 */ 182 static int 183 sun_prinfo_verify_sanity(struct audio_prinfo * prinfo) 184 { 185 return prinfo->precision >= 8 && prinfo->precision <= 32 && 186 prinfo->channels >= 1 && prinfo->channels < SUN_MAX_CHANNELS && 187 prinfo->sample_rate < SUN_MAX_RATE && 188 prinfo->sample_rate > SUN_MIN_RATE; 189 } 190 191 static int 192 sun_enumerate_devices(cubeb * context, cubeb_device_type type, 193 cubeb_device_collection * collection) 194 { 195 unsigned i; 196 cubeb_device_info device = {0}; 197 char dev[16] = SUN_DEFAULT_DEVICE; 198 char dev_friendly[64]; 199 struct audio_info hwfmt; 200 struct audio_device hwname; 201 struct audio_prinfo * prinfo = NULL; 202 int hwprops; 203 204 collection->device = calloc(SUN_DEVICE_COUNT, sizeof(cubeb_device_info)); 205 if (collection->device == NULL) { 206 return CUBEB_ERROR; 207 } 208 collection->count = 0; 209 210 for (i = 0; i < SUN_DEVICE_COUNT; ++i) { 211 if (i > 0) { 212 (void)snprintf(dev, sizeof(dev), "/dev/audio%u", i - 1); 213 } 214 if (sun_get_hwinfo(dev, &hwfmt, &hwprops, &hwname) != CUBEB_OK) { 215 continue; 216 } 217 #ifdef AUDIO_GETPROPS 218 device.type = 0; 219 if ((hwprops & AUDIO_PROP_CAPTURE) != 0 && 220 sun_prinfo_verify_sanity(&hwfmt.record)) { 221 /* the device supports recording, probably */ 222 device.type |= CUBEB_DEVICE_TYPE_INPUT; 223 } 224 if ((hwprops & AUDIO_PROP_PLAYBACK) != 0 && 225 sun_prinfo_verify_sanity(&hwfmt.play)) { 226 /* the device supports playback, probably */ 227 device.type |= CUBEB_DEVICE_TYPE_OUTPUT; 228 } 229 switch (device.type) { 230 case 0: 231 /* device doesn't do input or output, aliens probably involved */ 232 continue; 233 case CUBEB_DEVICE_TYPE_INPUT: 234 if ((type & CUBEB_DEVICE_TYPE_INPUT) == 0) { 235 /* this device is input only, not scanning for those, skip it */ 236 continue; 237 } 238 break; 239 case CUBEB_DEVICE_TYPE_OUTPUT: 240 if ((type & CUBEB_DEVICE_TYPE_OUTPUT) == 0) { 241 /* this device is output only, not scanning for those, skip it */ 242 continue; 243 } 244 break; 245 } 246 if ((type & CUBEB_DEVICE_TYPE_INPUT) != 0) { 247 prinfo = &hwfmt.record; 248 } 249 if ((type & CUBEB_DEVICE_TYPE_OUTPUT) != 0) { 250 prinfo = &hwfmt.play; 251 } 252 #endif 253 if (i > 0) { 254 (void)snprintf(dev_friendly, sizeof(dev_friendly), "%s %s %s (%d)", 255 hwname.name, hwname.version, hwname.config, i - 1); 256 } else { 257 (void)snprintf(dev_friendly, sizeof(dev_friendly), "%s %s %s (default)", 258 hwname.name, hwname.version, hwname.config); 259 } 260 device.devid = (void *)(uintptr_t)i; 261 device.device_id = strdup(dev); 262 device.friendly_name = strdup(dev_friendly); 263 device.group_id = strdup(dev); 264 device.vendor_name = strdup(hwname.name); 265 device.type = type; 266 device.state = CUBEB_DEVICE_STATE_ENABLED; 267 device.preferred = 268 (i == 0) ? CUBEB_DEVICE_PREF_ALL : CUBEB_DEVICE_PREF_NONE; 269 #ifdef AUDIO_GETFORMAT 270 device.max_channels = prinfo->channels; 271 device.default_rate = prinfo->sample_rate; 272 #else 273 device.max_channels = 2; 274 device.default_rate = SUN_PREFER_RATE; 275 #endif 276 device.default_format = CUBEB_DEVICE_FMT_S16NE; 277 device.format = CUBEB_DEVICE_FMT_S16NE; 278 device.min_rate = SUN_MIN_RATE; 279 device.max_rate = SUN_MAX_RATE; 280 device.latency_lo = SUN_LATENCY_MS * SUN_MIN_RATE / 1000; 281 device.latency_hi = SUN_LATENCY_MS * SUN_MAX_RATE / 1000; 282 collection->device[collection->count++] = device; 283 } 284 return CUBEB_OK; 285 } 286 287 static int 288 sun_device_collection_destroy(cubeb * context, 289 cubeb_device_collection * collection) 290 { 291 unsigned i; 292 293 for (i = 0; i < collection->count; ++i) { 294 free((char *)collection->device[i].device_id); 295 free((char *)collection->device[i].friendly_name); 296 free((char *)collection->device[i].group_id); 297 free((char *)collection->device[i].vendor_name); 298 } 299 free(collection->device); 300 return CUBEB_OK; 301 } 302 303 static int 304 sun_copy_params(int fd, cubeb_stream * stream, cubeb_stream_params * params, 305 struct audio_info * info, struct audio_prinfo * prinfo) 306 { 307 prinfo->channels = params->channels; 308 prinfo->sample_rate = params->rate; 309 #ifdef AUDIO_ENCODING_SLINEAR_LE 310 switch (params->format) { 311 case CUBEB_SAMPLE_S16LE: 312 prinfo->encoding = AUDIO_ENCODING_SLINEAR_LE; 313 prinfo->precision = 16; 314 break; 315 case CUBEB_SAMPLE_S16BE: 316 prinfo->encoding = AUDIO_ENCODING_SLINEAR_BE; 317 prinfo->precision = 16; 318 break; 319 case CUBEB_SAMPLE_FLOAT32NE: 320 prinfo->encoding = AUDIO_ENCODING_SLINEAR; 321 prinfo->precision = 32; 322 break; 323 default: 324 LOG("Unsupported format"); 325 return CUBEB_ERROR_INVALID_FORMAT; 326 } 327 #else 328 switch (params->format) { 329 case CUBEB_SAMPLE_S16NE: 330 prinfo->encoding = AUDIO_ENCODING_LINEAR; 331 prinfo->precision = 16; 332 break; 333 case CUBEB_SAMPLE_FLOAT32NE: 334 prinfo->encoding = AUDIO_ENCODING_LINEAR; 335 prinfo->precision = 32; 336 break; 337 default: 338 LOG("Unsupported format"); 339 return CUBEB_ERROR_INVALID_FORMAT; 340 } 341 #endif 342 if (ioctl(fd, AUDIO_SETINFO, info) == -1) { 343 return CUBEB_ERROR; 344 } 345 if (ioctl(fd, AUDIO_GETINFO, info) == -1) { 346 return CUBEB_ERROR; 347 } 348 return CUBEB_OK; 349 } 350 351 static int 352 sun_stream_stop(cubeb_stream * s) 353 { 354 pthread_mutex_lock(&s->mutex); 355 if (s->running) { 356 s->running = false; 357 pthread_mutex_unlock(&s->mutex); 358 pthread_join(s->thread, NULL); 359 } else { 360 pthread_mutex_unlock(&s->mutex); 361 } 362 return CUBEB_OK; 363 } 364 365 static void 366 sun_stream_destroy(cubeb_stream * s) 367 { 368 sun_stream_stop(s); 369 pthread_mutex_destroy(&s->mutex); 370 if (s->play.fd != -1) { 371 close(s->play.fd); 372 } 373 if (s->record.fd != -1) { 374 close(s->record.fd); 375 } 376 free(s->play.buf); 377 free(s->record.buf); 378 free(s); 379 } 380 381 static void 382 sun_float_to_linear32(void * buf, unsigned sample_count, float vol) 383 { 384 float * in = buf; 385 int32_t * out = buf; 386 int32_t * tail = out + sample_count; 387 388 while (out < tail) { 389 float f = *(in++) * vol; 390 if (f < -1.0) 391 f = -1.0; 392 else if (f > 1.0) 393 f = 1.0; 394 *(out++) = f * (float)INT32_MAX; 395 } 396 } 397 398 static void 399 sun_linear32_to_float(void * buf, unsigned sample_count) 400 { 401 int32_t * in = buf; 402 float * out = buf; 403 float * tail = out + sample_count; 404 405 while (out < tail) { 406 *(out++) = (1.0 / 0x80000000) * *(in++); 407 } 408 } 409 410 static void 411 sun_linear16_set_vol(int16_t * buf, unsigned sample_count, float vol) 412 { 413 unsigned i; 414 int32_t multiplier = vol * 0x8000; 415 416 for (i = 0; i < sample_count; ++i) { 417 buf[i] = (buf[i] * multiplier) >> 15; 418 } 419 } 420 421 static void * 422 sun_io_routine(void * arg) 423 { 424 cubeb_stream * s = arg; 425 cubeb_state state = CUBEB_STATE_STARTED; 426 size_t to_read = 0; 427 long to_write = 0; 428 size_t write_ofs = 0; 429 size_t read_ofs = 0; 430 int drain = 0; 431 432 CUBEB_REGISTER_THREAD("cubeb rendering thread"); 433 434 s->state_cb(s, s->user_ptr, CUBEB_STATE_STARTED); 435 while (state != CUBEB_STATE_ERROR) { 436 pthread_mutex_lock(&s->mutex); 437 if (!s->running) { 438 pthread_mutex_unlock(&s->mutex); 439 state = CUBEB_STATE_STOPPED; 440 break; 441 } 442 pthread_mutex_unlock(&s->mutex); 443 if (s->record.fd != -1 && s->record.floating) { 444 sun_linear32_to_float(s->record.buf, 445 s->record.info.record.channels * SUN_BUFFER_FRAMES); 446 } 447 to_write = s->data_cb(s, s->user_ptr, s->record.buf, s->play.buf, 448 SUN_BUFFER_FRAMES); 449 if (to_write == CUBEB_ERROR) { 450 state = CUBEB_STATE_ERROR; 451 break; 452 } 453 if (s->play.fd != -1) { 454 float vol; 455 456 pthread_mutex_lock(&s->mutex); 457 vol = s->volume; 458 pthread_mutex_unlock(&s->mutex); 459 460 if (s->play.floating) { 461 sun_float_to_linear32(s->play.buf, 462 s->play.info.play.channels * to_write, vol); 463 } else { 464 sun_linear16_set_vol(s->play.buf, s->play.info.play.channels * to_write, 465 vol); 466 } 467 } 468 if (to_write < SUN_BUFFER_FRAMES) { 469 drain = 1; 470 } 471 to_write = s->play.fd != -1 ? to_write : 0; 472 to_read = s->record.fd != -1 ? SUN_BUFFER_FRAMES : 0; 473 write_ofs = 0; 474 read_ofs = 0; 475 while (to_write > 0 || to_read > 0) { 476 size_t bytes; 477 ssize_t n, frames; 478 479 if (to_write > 0) { 480 bytes = to_write * s->play.frame_size; 481 if ((n = write(s->play.fd, (uint8_t *)s->play.buf + write_ofs, bytes)) < 482 0) { 483 state = CUBEB_STATE_ERROR; 484 break; 485 } 486 frames = n / s->play.frame_size; 487 pthread_mutex_lock(&s->mutex); 488 s->frames_written += frames; 489 pthread_mutex_unlock(&s->mutex); 490 to_write -= frames; 491 write_ofs += n; 492 } 493 if (to_read > 0) { 494 bytes = to_read * s->record.frame_size; 495 if ((n = read(s->record.fd, (uint8_t *)s->record.buf + read_ofs, 496 bytes)) < 0) { 497 state = CUBEB_STATE_ERROR; 498 break; 499 } 500 frames = n / s->record.frame_size; 501 to_read -= frames; 502 read_ofs += n; 503 } 504 } 505 if (drain && state != CUBEB_STATE_ERROR) { 506 state = CUBEB_STATE_DRAINED; 507 break; 508 } 509 } 510 s->state_cb(s, s->user_ptr, state); 511 CUBEB_UNREGISTER_THREAD(); 512 return NULL; 513 } 514 515 static int 516 sun_stream_init(cubeb * context, cubeb_stream ** stream, 517 char const * stream_name, cubeb_devid input_device, 518 cubeb_stream_params * input_stream_params, 519 cubeb_devid output_device, 520 cubeb_stream_params * output_stream_params, 521 unsigned latency_frames, cubeb_data_callback data_callback, 522 cubeb_state_callback state_callback, void * user_ptr) 523 { 524 int ret = CUBEB_OK; 525 cubeb_stream * s = NULL; 526 527 (void)stream_name; 528 (void)latency_frames; 529 if ((s = calloc(1, sizeof(cubeb_stream))) == NULL) { 530 ret = CUBEB_ERROR; 531 goto error; 532 } 533 s->record.fd = -1; 534 s->play.fd = -1; 535 if (input_device != 0) { 536 snprintf(s->record.name, sizeof(s->record.name), "/dev/audio%zu", 537 (uintptr_t)input_device - 1); 538 } else { 539 snprintf(s->record.name, sizeof(s->record.name), "%s", SUN_DEFAULT_DEVICE); 540 } 541 if (output_device != 0) { 542 snprintf(s->play.name, sizeof(s->play.name), "/dev/audio%zu", 543 (uintptr_t)output_device - 1); 544 } else { 545 snprintf(s->play.name, sizeof(s->play.name), "%s", SUN_DEFAULT_DEVICE); 546 } 547 if (input_stream_params != NULL) { 548 if (input_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { 549 LOG("Loopback not supported"); 550 ret = CUBEB_ERROR_NOT_SUPPORTED; 551 goto error; 552 } 553 if (s->record.fd == -1) { 554 if ((s->record.fd = open(s->record.name, O_RDONLY)) == -1) { 555 LOG("Audio device could not be opened as read-only"); 556 ret = CUBEB_ERROR_DEVICE_UNAVAILABLE; 557 goto error; 558 } 559 } 560 AUDIO_INITINFO(&s->record.info); 561 #ifdef AUMODE_RECORD 562 s->record.info.mode = AUMODE_RECORD; 563 #endif 564 if ((ret = sun_copy_params(s->record.fd, s, input_stream_params, 565 &s->record.info, &s->record.info.record)) != 566 CUBEB_OK) { 567 LOG("Setting record params failed"); 568 goto error; 569 } 570 s->record.floating = 571 (input_stream_params->format == CUBEB_SAMPLE_FLOAT32NE); 572 } 573 if (output_stream_params != NULL) { 574 if (output_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { 575 LOG("Loopback not supported"); 576 ret = CUBEB_ERROR_NOT_SUPPORTED; 577 goto error; 578 } 579 if (s->play.fd == -1) { 580 if ((s->play.fd = open(s->play.name, O_WRONLY)) == -1) { 581 LOG("Audio device could not be opened as write-only"); 582 ret = CUBEB_ERROR_DEVICE_UNAVAILABLE; 583 goto error; 584 } 585 } 586 AUDIO_INITINFO(&s->play.info); 587 #ifdef AUMODE_PLAY 588 s->play.info.mode = AUMODE_PLAY; 589 #endif 590 if ((ret = sun_copy_params(s->play.fd, s, output_stream_params, 591 &s->play.info, &s->play.info.play)) != 592 CUBEB_OK) { 593 LOG("Setting play params failed"); 594 goto error; 595 } 596 s->play.floating = (output_stream_params->format == CUBEB_SAMPLE_FLOAT32NE); 597 } 598 s->context = context; 599 s->volume = 1.0; 600 s->state_cb = state_callback; 601 s->data_cb = data_callback; 602 s->user_ptr = user_ptr; 603 if (pthread_mutex_init(&s->mutex, NULL) != 0) { 604 LOG("Failed to create mutex"); 605 goto error; 606 } 607 s->play.frame_size = 608 s->play.info.play.channels * (s->play.info.play.precision / 8); 609 if (s->play.fd != -1 && 610 (s->play.buf = calloc(SUN_BUFFER_FRAMES, s->play.frame_size)) == NULL) { 611 ret = CUBEB_ERROR; 612 goto error; 613 } 614 s->record.frame_size = 615 s->record.info.record.channels * (s->record.info.record.precision / 8); 616 if (s->record.fd != -1 && 617 (s->record.buf = calloc(SUN_BUFFER_FRAMES, s->record.frame_size)) == 618 NULL) { 619 ret = CUBEB_ERROR; 620 goto error; 621 } 622 *stream = s; 623 return CUBEB_OK; 624 error: 625 if (s != NULL) { 626 sun_stream_destroy(s); 627 } 628 return ret; 629 } 630 631 static int 632 sun_stream_start(cubeb_stream * s) 633 { 634 s->running = true; 635 if (pthread_create(&s->thread, NULL, sun_io_routine, s) != 0) { 636 LOG("Couldn't create thread"); 637 return CUBEB_ERROR; 638 } 639 return CUBEB_OK; 640 } 641 642 static int 643 sun_stream_get_position(cubeb_stream * s, uint64_t * position) 644 { 645 #ifdef AUDIO_GETOOFFS 646 struct audio_offset offset; 647 648 if (ioctl(s->play.fd, AUDIO_GETOOFFS, &offset) == -1) { 649 return CUBEB_ERROR; 650 } 651 s->blocks_written += offset.deltablks; 652 *position = (s->blocks_written * s->play.info.blocksize) / s->play.frame_size; 653 return CUBEB_OK; 654 #else 655 pthread_mutex_lock(&s->mutex); 656 *position = s->frames_written; 657 pthread_mutex_unlock(&s->mutex); 658 return CUBEB_OK; 659 #endif 660 } 661 662 static int 663 sun_stream_get_latency(cubeb_stream * s, uint32_t * latency) 664 { 665 #ifdef AUDIO_GETBUFINFO 666 struct audio_info info; 667 668 if (ioctl(s->play.fd, AUDIO_GETBUFINFO, &info) == -1) { 669 return CUBEB_ERROR; 670 } 671 672 *latency = (info.play.seek + info.blocksize) / s->play.frame_size; 673 return CUBEB_OK; 674 #else 675 cubeb_stream_params params; 676 677 params.rate = s->play.info.play.sample_rate; 678 679 return sun_get_min_latency(NULL, params, latency); 680 #endif 681 } 682 683 static int 684 sun_stream_set_volume(cubeb_stream * stream, float volume) 685 { 686 pthread_mutex_lock(&stream->mutex); 687 stream->volume = volume; 688 pthread_mutex_unlock(&stream->mutex); 689 return CUBEB_OK; 690 } 691 692 static int 693 sun_get_current_device(cubeb_stream * stream, cubeb_device ** const device) 694 { 695 *device = calloc(1, sizeof(cubeb_device)); 696 if (*device == NULL) { 697 return CUBEB_ERROR; 698 } 699 (*device)->input_name = 700 stream->record.fd != -1 ? strdup(stream->record.name) : NULL; 701 (*device)->output_name = 702 stream->play.fd != -1 ? strdup(stream->play.name) : NULL; 703 return CUBEB_OK; 704 } 705 706 static int 707 sun_stream_device_destroy(cubeb_stream * stream, cubeb_device * device) 708 { 709 (void)stream; 710 free(device->input_name); 711 free(device->output_name); 712 free(device); 713 return CUBEB_OK; 714 } 715 716 static struct cubeb_ops const sun_ops = { 717 .init = sun_init, 718 .get_backend_id = sun_get_backend_id, 719 .get_max_channel_count = sun_get_max_channel_count, 720 .get_min_latency = sun_get_min_latency, 721 .get_preferred_sample_rate = sun_get_preferred_sample_rate, 722 .get_supported_input_processing_params = NULL, 723 .enumerate_devices = sun_enumerate_devices, 724 .device_collection_destroy = sun_device_collection_destroy, 725 .destroy = sun_destroy, 726 .stream_init = sun_stream_init, 727 .stream_destroy = sun_stream_destroy, 728 .stream_start = sun_stream_start, 729 .stream_stop = sun_stream_stop, 730 .stream_get_position = sun_stream_get_position, 731 .stream_get_latency = sun_stream_get_latency, 732 .stream_get_input_latency = NULL, 733 .stream_set_volume = sun_stream_set_volume, 734 .stream_set_name = NULL, 735 .stream_get_current_device = sun_get_current_device, 736 .stream_set_input_mute = NULL, 737 .stream_set_input_processing_params = NULL, 738 .stream_device_destroy = sun_stream_device_destroy, 739 .stream_register_device_changed_callback = NULL, 740 .register_device_collection_changed = NULL};